From 6a207bc1e09bc8bb2097a17cdb270dee04ac9221 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Fri, 29 Apr 2022 21:37:04 +0200 Subject: [PATCH 01/55] feat: allow setting search params to custom value This is useful for getting results other than videos, like playlists and channels. --- lib/Innertube.js | 1 + lib/core/Actions.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Innertube.js b/lib/Innertube.js index 89fdc2aeb..706a7c0cd 100644 --- a/lib/Innertube.js +++ b/lib/Innertube.js @@ -411,6 +411,7 @@ class Innertube { * * @param {string} query - Search query. * @param {object} options - Search options. + * @param {string} option.params - Pre-defined search parameter. * @param {string} options.client - Client used to perform the search, can be: `YTMUSIC` or `YOUTUBE`. * @param {string} options.period - Filter videos uploaded within a period, can be: any | hour | day | week | month | year * @param {string} options.order - Filter results by order, can be: relevance | rating | age | views diff --git a/lib/core/Actions.js b/lib/core/Actions.js index e72c7f3e0..11b153cf7 100644 --- a/lib/core/Actions.js +++ b/lib/core/Actions.js @@ -283,7 +283,7 @@ async function search(session, client, args = {}) { switch (client) { case 'YOUTUBE': if (args.query) { - data.params = Proto.encodeSearchFilter(args.options.period, args.options.duration, args.options.order); + data.params = args.options.hasOwnProperty('params') ? args.options.params : Proto.encodeSearchFilter(args.options.period, args.options.duration, args.options.order); data.query = args.query; } else { data.continuation = args.ctoken; From a04deb1cc5013117d40bda434b1d04c4ebe33a9f Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Fri, 29 Apr 2022 21:39:34 +0200 Subject: [PATCH 02/55] feat: add initial parsers for common renderers --- lib/parser/contents/Author.js | 45 +++++++++++++++++++ lib/parser/contents/Channel.js | 21 +++++++++ lib/parser/contents/ChildVideo.js | 14 ++++++ lib/parser/contents/GenericList.js | 12 +++++ lib/parser/contents/HorizontalList.js | 12 +++++ lib/parser/contents/MetadataBadge.js | 16 +++++++ lib/parser/contents/Mix.js | 15 +++++++ lib/parser/contents/NavigatableText.js | 25 +++++++++++ lib/parser/contents/NavigationEndpoint.js | 14 ++++++ lib/parser/contents/Playlist.js | 20 +++++++++ lib/parser/contents/Shelf.js | 13 ++++++ lib/parser/contents/Text.js | 25 +++++++++++ lib/parser/contents/Thumbnail.js | 16 +++++++ lib/parser/contents/TwoColumnSearchResults.js | 13 ++++++ lib/parser/contents/VerticalList.js | 12 +++++ lib/parser/contents/Video.js | 45 +++++++++++++++++++ lib/parser/contents/index.js | 38 ++++++++++++++++ 17 files changed, 356 insertions(+) create mode 100644 lib/parser/contents/Author.js create mode 100644 lib/parser/contents/Channel.js create mode 100644 lib/parser/contents/ChildVideo.js create mode 100644 lib/parser/contents/GenericList.js create mode 100644 lib/parser/contents/HorizontalList.js create mode 100644 lib/parser/contents/MetadataBadge.js create mode 100644 lib/parser/contents/Mix.js create mode 100644 lib/parser/contents/NavigatableText.js create mode 100644 lib/parser/contents/NavigationEndpoint.js create mode 100644 lib/parser/contents/Playlist.js create mode 100644 lib/parser/contents/Shelf.js create mode 100644 lib/parser/contents/Text.js create mode 100644 lib/parser/contents/Thumbnail.js create mode 100644 lib/parser/contents/TwoColumnSearchResults.js create mode 100644 lib/parser/contents/VerticalList.js create mode 100644 lib/parser/contents/Video.js create mode 100644 lib/parser/contents/index.js diff --git a/lib/parser/contents/Author.js b/lib/parser/contents/Author.js new file mode 100644 index 000000000..b06f697fb --- /dev/null +++ b/lib/parser/contents/Author.js @@ -0,0 +1,45 @@ +const ResultsParser = require("."); +const NavigatableText = require("./NavigatableText"); +const Thumbnail = require("./Thumbnail"); + +class Author { + #navText; + badges; + constructor(item, badges, thumbs) { + this.#navText = new NavigatableText(item); + this.badges = Array.isArray(badges) ? ResultsParser.parse(badges) : []; + if (thumbs) { + this.thumbnails = Thumbnail.fromResponse(thumbs); + } + else { + this.thumbnails = []; + } + } + + get name() { + return this.#navText.text; + } + + get id() { + // XXX: maybe confirm that pageType == "WEB_PAGE_TYPE_CHANNEL"? + return this.#navText.endpoint.browseId; + } + + get url() { + return this.#navText.endpoint.url; + } + + get isVerified() { + return this.badges.some(badge => badge.style === 'BADGE_STYLE_TYPE_VERIFIED'); + } + + get isVerifiedArtist() { + return this.badges.some(badge => badge.style === 'BADGE_STYLE_TYPE_VERIFIED_ARTIST'); + } + + get bestThumbnail() { + return this.thumbnails[0]; + } +} + +module.exports = Author; \ No newline at end of file diff --git a/lib/parser/contents/Channel.js b/lib/parser/contents/Channel.js new file mode 100644 index 000000000..a3bd2b7d6 --- /dev/null +++ b/lib/parser/contents/Channel.js @@ -0,0 +1,21 @@ +const ResultsParser = require("."); +const Author = require("./Author"); +const Text = require("./Text"); + +class Channel { + type = 'Channel'; + + constructor(item) { + this.id = item.channelId; + this.author = new Author({ + ...item.title, + navigationEndpoint: item.navigationEndpoint + }, item.ownerBadges, item.thumbnail); + this.subscribers = new Text(item.subscriberCountText).text; + this.descriptionSnippet = new Text(item.descriptionSnippet).text; + this.videos = new Text(item.videoCountText).text; + + } +} + +module.exports = Channel; \ No newline at end of file diff --git a/lib/parser/contents/ChildVideo.js b/lib/parser/contents/ChildVideo.js new file mode 100644 index 000000000..14f22c34c --- /dev/null +++ b/lib/parser/contents/ChildVideo.js @@ -0,0 +1,14 @@ +const ResultsParser = require("."); +const Text = require("./Text"); + +class ChildVideo { + type = 'ChildVideo'; + + constructor(item) { + this.id = item.videoId; + this.title = new Text(item.title).text; + this.length = new Text(item.lengthText).text; + } +} + +module.exports = ChildVideo; \ No newline at end of file diff --git a/lib/parser/contents/GenericList.js b/lib/parser/contents/GenericList.js new file mode 100644 index 000000000..13efab220 --- /dev/null +++ b/lib/parser/contents/GenericList.js @@ -0,0 +1,12 @@ + +const ResultsParser = require("."); + +module.exports = (name) => { + return class List { + type = name; + isList = true; + constructor(items) { + this.contents = ResultsParser.parse(items.contents); + } + } +} \ No newline at end of file diff --git a/lib/parser/contents/HorizontalList.js b/lib/parser/contents/HorizontalList.js new file mode 100644 index 000000000..12178884c --- /dev/null +++ b/lib/parser/contents/HorizontalList.js @@ -0,0 +1,12 @@ +const ResultsParser = require("."); + +class HorizontalList { + type = 'HorizontalList'; + + constructor(item) { + this.visibleItemCount = item.visibleItemCount; + this.items = ResultsParser.parse(item.items); + } +} + +module.exports = HorizontalList; \ No newline at end of file diff --git a/lib/parser/contents/MetadataBadge.js b/lib/parser/contents/MetadataBadge.js new file mode 100644 index 000000000..a9d2fa36e --- /dev/null +++ b/lib/parser/contents/MetadataBadge.js @@ -0,0 +1,16 @@ +const ResultsParser = require("."); + +class MetadataBadge { + type = 'MetadataBadge'; + style; + label; + nonAbbreviatedLabel; + + constructor(item) { + this.style = item.style; + this.label = item.label; + this.nonAbbreviatedLabel = item.accessibilityData?.label; + } +} + +module.exports = MetadataBadge; \ No newline at end of file diff --git a/lib/parser/contents/Mix.js b/lib/parser/contents/Mix.js new file mode 100644 index 000000000..b1e70b905 --- /dev/null +++ b/lib/parser/contents/Mix.js @@ -0,0 +1,15 @@ +const ResultsParser = require("."); +const Playlist = require("./Playlist"); +const Thumbnail = require("./Thumbnail"); + +class Mix extends Playlist { + type = 'Mix'; + + constructor(item) { + super(item); + delete this.thumbnails; + this.thumbnail = Thumbnail.fromResponse(item.thumbnail); + } +} + +module.exports = Mix; \ No newline at end of file diff --git a/lib/parser/contents/NavigatableText.js b/lib/parser/contents/NavigatableText.js new file mode 100644 index 000000000..fcef6f651 --- /dev/null +++ b/lib/parser/contents/NavigatableText.js @@ -0,0 +1,25 @@ +const Text = require("./Text"); +const NavigationEndpoint = require("./NavigationEndpoint"); + +class NavigatableText extends Text { + type = 'NavigatableText'; + endpoint; + constructor(node) { + super(node); + this.endpoint = + node.runs?.[0]?.navigationEndpoint ? + new NavigationEndpoint(node.runs[0].navigationEndpoint) : + node.navigationEndpoint ? + new NavigationEndpoint(node.navigationEndpoint) : null; + } + + toString() { + return `[${this.text}](${this.url?.toString()})`; + } + + toJSON() { + return this; + } +} + +module.exports = NavigatableText; diff --git a/lib/parser/contents/NavigationEndpoint.js b/lib/parser/contents/NavigationEndpoint.js new file mode 100644 index 000000000..f90ed9c83 --- /dev/null +++ b/lib/parser/contents/NavigationEndpoint.js @@ -0,0 +1,14 @@ +class NavigationEndpoint { + type = 'NavigationEndpoint'; + url; + browseId; + constructor (item) { + this.url = + item.browseEndpoint.canonicalBaseUrl || + item.commandMetadata.webCommandMetadata.url; + this.browseId = item.browseEndpoint.browseId; + this.pageType = item.commandMetadata.webCommandMetadata.webPageType || 'UNKNOWN'; + } +} + +module.exports = NavigationEndpoint; \ No newline at end of file diff --git a/lib/parser/contents/Playlist.js b/lib/parser/contents/Playlist.js new file mode 100644 index 000000000..f15dd6c6b --- /dev/null +++ b/lib/parser/contents/Playlist.js @@ -0,0 +1,20 @@ +const ResultsParser = require("."); +const Author = require("./Author"); +const Text = require("./Text"); +const Thumbnail = require("./Thumbnail"); + +class Playlist { + type = 'Playlist'; + + constructor(item) { + this.id = item.playlistId; + this.title = new Text(item.title).text; + this.author = item.longBylineText.simpleText ? null : new Author(item.longBylineText, item.ownerBadges); + this.thumbnails = Array.isArray(item.thumbnails) ? item.thumbnails.map(thumbs => Thumbnail.fromResponse(thumbs)) : []; + if (new Text(item.videoCountText).text !== 'Mix') + this.videos = parseInt(item.videoCount); + this.firstVideos = ResultsParser.parse(item.videos); + } +} + +module.exports = Playlist; \ No newline at end of file diff --git a/lib/parser/contents/Shelf.js b/lib/parser/contents/Shelf.js new file mode 100644 index 000000000..e43d2f4b7 --- /dev/null +++ b/lib/parser/contents/Shelf.js @@ -0,0 +1,13 @@ +const ResultsParser = require("."); +const Text = require("./Text"); + +class Shelf { + type = 'Shelf'; + + constructor(item) { + this.title = new Text(item.title).text; + this.content = ResultsParser.parseItem(item.content); + } +} + +module.exports = Shelf; \ No newline at end of file diff --git a/lib/parser/contents/Text.js b/lib/parser/contents/Text.js new file mode 100644 index 000000000..8f84fee15 --- /dev/null +++ b/lib/parser/contents/Text.js @@ -0,0 +1,25 @@ +class Text { + type = 'Text'; + text; + constructor(txt, def = null) { + if (typeof txt !== 'object') { + this.text = def; + } + else if (txt.hasOwnProperty('simpleText')) + this.text = txt.simpleText; + else if (Array.isArray(txt.runs)) { + this.text = txt.runs.map(a => a.text).join(''); + } + else this.text = def; + } + + toString() { + return this.text; + } + + toJSON() { + return this.text; + } +} + +module.exports = Text; \ No newline at end of file diff --git a/lib/parser/contents/Thumbnail.js b/lib/parser/contents/Thumbnail.js new file mode 100644 index 000000000..27b7af3b0 --- /dev/null +++ b/lib/parser/contents/Thumbnail.js @@ -0,0 +1,16 @@ +class Thumbnail { + constructor ({ url, width, height }) { + this.url = url; + this.width = width; + this.height = height; + } + + static fromResponse({ thumbnails }) { + if (!thumbnails) { + return; + } + return thumbnails.map(x => new Thumbnail(x)).sort((a, b) => b.width - a.width); + } +} + +module.exports = Thumbnail; \ No newline at end of file diff --git a/lib/parser/contents/TwoColumnSearchResults.js b/lib/parser/contents/TwoColumnSearchResults.js new file mode 100644 index 000000000..49b818fa4 --- /dev/null +++ b/lib/parser/contents/TwoColumnSearchResults.js @@ -0,0 +1,13 @@ +const ResultsParser = require("."); + +class TwoColumnSearchResults { + type = 'TwoColumnSearchResults'; + + constructor(items) { + this.primary = ResultsParser.parseItem(items.primaryContents); + if (items.secondaryContents) + this.secondary = ResultsParser.parseItem(items.secondaryContents); + } +} + +module.exports = TwoColumnSearchResults; \ No newline at end of file diff --git a/lib/parser/contents/VerticalList.js b/lib/parser/contents/VerticalList.js new file mode 100644 index 000000000..b350be481 --- /dev/null +++ b/lib/parser/contents/VerticalList.js @@ -0,0 +1,12 @@ +const ResultsParser = require("."); + +class VerticalList { + type = 'VerticalList'; + + constructor(item) { + this.collapsedItemCount = item.collapsedItemCount; + this.items = ResultsParser.parse(item.items); + } +} + +module.exports = VerticalList; \ No newline at end of file diff --git a/lib/parser/contents/Video.js b/lib/parser/contents/Video.js new file mode 100644 index 000000000..a0371f52e --- /dev/null +++ b/lib/parser/contents/Video.js @@ -0,0 +1,45 @@ +const ResultsParser = require("."); +const Author = require("./Author"); +const Text = require("./Text"); +const Thumbnail = require("./Thumbnail"); + +class Video { + type = 'Video'; + + constructor(item) { + this.author = new Author(item.ownerText, item.ownerBadges, item.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail); + this.badges = Array.isArray(item.badges) ? ResultsParser.parse(item.badges) : []; + this.thumbnails = Thumbnail.fromResponse(item); + const upcoming = item.upcomingEventData ? Number(`${item.upcomingEventData.startTime}000`) : null; + if (upcoming) this.upcoming = new Date(upcoming); + this.id = item.videoId; + this.title = new Text(item.title, '').text; + const lengthAlt = item.thumbnailOverlays.find(overlay => overlay.hasOwnProperty('thumbnailOverlayTimeStatusRenderer'))?.thumbnailOverlayTimeStatusRenderer; + this.duration = item.lengthText ? new Text(item.lengthText, '').text : lengthAlt?.text ? new Text(lengthAlt.text).text : ''; + this.publishedAt = new Text(item.publishedTimeText, '').text; + this.views = new Text(item.viewCountText, '').text; + // TODO: might be simplified? this appears to only contain the description + this.snippets = item?.detailedMetadataSnippets?.map(snip => ({ + text: new Text(snip.snippetText, '').text, + hoverText: new Text(snip.snippetHoverText, '').text, + })); + } + + get isLive() { + return this.badges.some(badge => badge.style === 'BADGE_STYLE_TYPE_LIVE_NOW'); + } + + get isUpcoming() { + return this.upcoming && this.upcoming > new Date(); + } + + get hasCaptions() { + return this.badges.some(badge => badge.label === 'CC'); + } + + get bestThumbnail() { + return this.thumbnails[0]; + } +} + +module.exports = Video; \ No newline at end of file diff --git a/lib/parser/contents/index.js b/lib/parser/contents/index.js new file mode 100644 index 000000000..0cf234725 --- /dev/null +++ b/lib/parser/contents/index.js @@ -0,0 +1,38 @@ +class ResultsParser { + static parse(results) { + return results.map((item) => this.parseItem(item)).filter((item) => item); + } + + static parseItem(item) { + const renderers = { + twoColumnSearchResultsRenderer: require('./TwoColumnSearchResults'), + videoRenderer: require('./Video'), + metadataBadgeRenderer: require('./MetadataBadge'), + channelRenderer: require('./Channel'), + playlistRenderer: require('./Playlist'), + childVideoRenderer: require('./ChildVideo'), + radioRenderer: require('./Mix'), + shelfRenderer: require('./Shelf'), + verticalListRenderer: require('./VerticalList'), + horizontalListRenderer: require('./HorizontalList'), + sectionListRenderer: require('./GenericList')('SectionList'), + secondarySearchContainerRenderer: require('./GenericList')('SecondarySearchContainer'), + itemSectionRenderer: require('./GenericList')('SecondarySearchContainer'), + } + + const keys = Reflect.ownKeys(item); + if (keys.length !== 1) return null; + + if (!renderers.hasOwnProperty(keys[0])) { + console.warn('No renderer found for type: ', keys[0]); + return null; + } + + const result = new renderers[keys[0]](item[keys[0]]); + if (keys[0] === 'shelfRenderer') + console.log(result); + return result; + } +} + +module.exports = ResultsParser; \ No newline at end of file From fb5b4cbf8f2254ca02e78ba5e6caf6f08d17ef2c Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Fri, 29 Apr 2022 23:22:56 +0200 Subject: [PATCH 03/55] feat: artist search renderers Added common renderers used when searching artists --- lib/parser/contents/Author.js | 4 ++++ lib/parser/contents/CollageHeroImage.js | 14 +++++++++++++ lib/parser/contents/GenericList.js | 4 ++-- lib/parser/contents/HorizontalCardList.js | 13 ++++++++++++ lib/parser/contents/NavigatableText.js | 4 +++- lib/parser/contents/NavigationEndpoint.js | 12 +++++++++-- lib/parser/contents/SearchRefinementCard.js | 18 ++++++++++++++++ lib/parser/contents/UniversalWatchCard.js | 13 ++++++++++++ lib/parser/contents/WatchCardCompactVideo.js | 21 +++++++++++++++++++ lib/parser/contents/WatchCardHeroVideo.js | 13 ++++++++++++ lib/parser/contents/WatchCardRichHeader.js | 16 ++++++++++++++ .../contents/WatchCardSectionSequence.js | 12 +++++++++++ lib/parser/contents/index.js | 9 ++++++++ 13 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 lib/parser/contents/CollageHeroImage.js create mode 100644 lib/parser/contents/HorizontalCardList.js create mode 100644 lib/parser/contents/SearchRefinementCard.js create mode 100644 lib/parser/contents/UniversalWatchCard.js create mode 100644 lib/parser/contents/WatchCardCompactVideo.js create mode 100644 lib/parser/contents/WatchCardHeroVideo.js create mode 100644 lib/parser/contents/WatchCardRichHeader.js create mode 100644 lib/parser/contents/WatchCardSectionSequence.js diff --git a/lib/parser/contents/Author.js b/lib/parser/contents/Author.js index b06f697fb..cda9ddc42 100644 --- a/lib/parser/contents/Author.js +++ b/lib/parser/contents/Author.js @@ -20,6 +20,10 @@ class Author { return this.#navText.text; } + set name(name) { + this.#navText.text = name; + } + get id() { // XXX: maybe confirm that pageType == "WEB_PAGE_TYPE_CHANNEL"? return this.#navText.endpoint.browseId; diff --git a/lib/parser/contents/CollageHeroImage.js b/lib/parser/contents/CollageHeroImage.js new file mode 100644 index 000000000..2310f8607 --- /dev/null +++ b/lib/parser/contents/CollageHeroImage.js @@ -0,0 +1,14 @@ +const ResultsParser = require("."); +const Thumbnail = require("./Thumbnail"); + +class CollageHeroImage { + type = 'CollageHeroImage'; + + constructor(item) { + this.left = Thumbnail.fromResponse(item.leftThumbnail); + this.topRight = Thumbnail.fromResponse(item.topRightThumbnail); + this.bottomRight = Thumbnail.fromResponse(item.bottomRightThumbnail); + } +} + +module.exports = CollageHeroImage; \ No newline at end of file diff --git a/lib/parser/contents/GenericList.js b/lib/parser/contents/GenericList.js index 13efab220..a12ecdf59 100644 --- a/lib/parser/contents/GenericList.js +++ b/lib/parser/contents/GenericList.js @@ -1,12 +1,12 @@ const ResultsParser = require("."); -module.exports = (name) => { +module.exports = (name, field = 'contents') => { return class List { type = name; isList = true; constructor(items) { - this.contents = ResultsParser.parse(items.contents); + this.contents = ResultsParser.parse(items[field]); } } } \ No newline at end of file diff --git a/lib/parser/contents/HorizontalCardList.js b/lib/parser/contents/HorizontalCardList.js new file mode 100644 index 000000000..3a5c34b2e --- /dev/null +++ b/lib/parser/contents/HorizontalCardList.js @@ -0,0 +1,13 @@ +const ResultsParser = require("."); +const Text = require("./Text"); + +class HorizontalCardList { + type = 'HorizontalCardList'; + + constructor(item) { + this.cards = ResultsParser.parse(item.cards); + this.header = item.header?.title ? new Text(item.header).text : null; + } +} + +module.exports = HorizontalCardList; \ No newline at end of file diff --git a/lib/parser/contents/NavigatableText.js b/lib/parser/contents/NavigatableText.js index fcef6f651..f7c2b3c3b 100644 --- a/lib/parser/contents/NavigatableText.js +++ b/lib/parser/contents/NavigatableText.js @@ -10,7 +10,9 @@ class NavigatableText extends Text { node.runs?.[0]?.navigationEndpoint ? new NavigationEndpoint(node.runs[0].navigationEndpoint) : node.navigationEndpoint ? - new NavigationEndpoint(node.navigationEndpoint) : null; + new NavigationEndpoint(node.navigationEndpoint) : + node.titleNavigationEndpoint ? + new NavigationEndpoint(node.titleNavigationEndpoint) : null; } toString() { diff --git a/lib/parser/contents/NavigationEndpoint.js b/lib/parser/contents/NavigationEndpoint.js index f90ed9c83..3bab8800b 100644 --- a/lib/parser/contents/NavigationEndpoint.js +++ b/lib/parser/contents/NavigationEndpoint.js @@ -4,9 +4,17 @@ class NavigationEndpoint { browseId; constructor (item) { this.url = - item.browseEndpoint.canonicalBaseUrl || + item.browseEndpoint?.canonicalBaseUrl || item.commandMetadata.webCommandMetadata.url; - this.browseId = item.browseEndpoint.browseId; + // TODO: clean this up! + // browseId is most likely the channel id + this.browseId = item.browseEndpoint?.browseId; + // this is the video id to navigate to + this.watchVideoId = item.watchEndpoint?.videoId; + // this is a playlist page to navigate to + // but redirect and actually start playing it + // see url for index (playnext and index searchParams) + this.watchPlaylistId = item.watchPlaylistEndpoint?.playlistId; this.pageType = item.commandMetadata.webCommandMetadata.webPageType || 'UNKNOWN'; } } diff --git a/lib/parser/contents/SearchRefinementCard.js b/lib/parser/contents/SearchRefinementCard.js new file mode 100644 index 000000000..981a304d9 --- /dev/null +++ b/lib/parser/contents/SearchRefinementCard.js @@ -0,0 +1,18 @@ +const ResultsParser = require("."); +const NavigationEndpoint = require("./NavigationEndpoint"); +const Text = require("./Text"); +const Thumbnail = require("./Thumbnail"); + +class SearchRefinementCard { + type = 'SearchRefinementCard'; + + constructor(item) { + this.thumbnail = Thumbnail.fromResponse(item.thumbnail); + this.endpoint = new NavigationEndpoint(item.searchEndpoint); + this.query = new Text(item.query).text; + // XXX: is this actually useful? + this.style = item.searchRefinementCardRendererStyle?.value; + } +} + +module.exports = SearchRefinementCard; \ No newline at end of file diff --git a/lib/parser/contents/UniversalWatchCard.js b/lib/parser/contents/UniversalWatchCard.js new file mode 100644 index 000000000..79f02718b --- /dev/null +++ b/lib/parser/contents/UniversalWatchCard.js @@ -0,0 +1,13 @@ +const ResultsParser = require("."); + +class UniversalWatchCard { + type = 'UniversalWatchCard'; + + constructor(items) { + this.header = ResultsParser.parseItem(items.header); + this.hero = ResultsParser.parseItem(items.callToAction); + this.sections = ResultsParser.parse(items.sections); + } +} + +module.exports = UniversalWatchCard; \ No newline at end of file diff --git a/lib/parser/contents/WatchCardCompactVideo.js b/lib/parser/contents/WatchCardCompactVideo.js new file mode 100644 index 000000000..3c1b9f057 --- /dev/null +++ b/lib/parser/contents/WatchCardCompactVideo.js @@ -0,0 +1,21 @@ +const ResultsParser = require("."); +const NavigatableText = require("./NavigatableText"); +const NavigationEndpoint = require("./NavigationEndpoint"); +const Text = require("./Text"); + +class WatchCardCompactVideo { + type = 'WatchCardCompactVideo'; + + constructor(item) { + this.title = new Text(item.title).text; + const [ views, publishedAt ] = new Text(item.subtitle).text.split('•'); + this.views = views.trim(); + this.publishedAt = publishedAt?.trim(); + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + this.duration = new Text(item.lengthText).text; + // TODO: byline is author? + this.byline = new NavigatableText(item.byline); + } +} + +module.exports = WatchCardCompactVideo; \ No newline at end of file diff --git a/lib/parser/contents/WatchCardHeroVideo.js b/lib/parser/contents/WatchCardHeroVideo.js new file mode 100644 index 000000000..f2046b77b --- /dev/null +++ b/lib/parser/contents/WatchCardHeroVideo.js @@ -0,0 +1,13 @@ +const ResultsParser = require("."); +const NavigationEndpoint = require("./NavigationEndpoint"); + +class WatchCardHeroVideo { + type = 'WatchCardHeroVideo'; + + constructor(item) { + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + this.collage = ResultsParser.parseItem(item.heroImage); + } +} + +module.exports = WatchCardHeroVideo; \ No newline at end of file diff --git a/lib/parser/contents/WatchCardRichHeader.js b/lib/parser/contents/WatchCardRichHeader.js new file mode 100644 index 000000000..486f6fbe5 --- /dev/null +++ b/lib/parser/contents/WatchCardRichHeader.js @@ -0,0 +1,16 @@ +const ResultsParser = require("."); +const Author = require("./Author"); +const Text = require("./Text"); + +class WatchCardRichHeader { + type = 'WatchCardRichHeader'; + + constructor(item) { + this.title = new Text(item.title).text; + this.subtitle = new Text(item.subtitle).text; + this.author = new Author(item, [item.titleBadge], item.avatar); + this.author.name = this.title; + } +} + +module.exports = WatchCardRichHeader; \ No newline at end of file diff --git a/lib/parser/contents/WatchCardSectionSequence.js b/lib/parser/contents/WatchCardSectionSequence.js new file mode 100644 index 000000000..dd6503399 --- /dev/null +++ b/lib/parser/contents/WatchCardSectionSequence.js @@ -0,0 +1,12 @@ +const ResultsParser = require("."); +const Text = require("./Text"); + +class WatchCardSectionSequence { + type = 'WatchCardSectionSequence'; + + constructor(item) { + this.lists = ResultsParser.parse(item.lists); + } +} + +module.exports = WatchCardSectionSequence; \ No newline at end of file diff --git a/lib/parser/contents/index.js b/lib/parser/contents/index.js index 0cf234725..53a7b6b3e 100644 --- a/lib/parser/contents/index.js +++ b/lib/parser/contents/index.js @@ -18,6 +18,15 @@ class ResultsParser { sectionListRenderer: require('./GenericList')('SectionList'), secondarySearchContainerRenderer: require('./GenericList')('SecondarySearchContainer'), itemSectionRenderer: require('./GenericList')('SecondarySearchContainer'), + universalWatchCardRenderer: require('./UniversalWatchCard'), + watchCardRichHeaderRenderer: require('./WatchCardRichHeader'), + watchCardHeroVideoRenderer: require('./WatchCardHeroVideo'), + collageHeroImageRenderer: require('./CollageHeroImage'), + watchCardSectionSequenceRenderer: require('./WatchCardSectionSequence'), + verticalWatchCardListRenderer: require('./GenericList')('VerticalWatchCardList', 'items'), + horizontalCardListRenderer: require('./HorizontalCardList'), + watchCardCompactVideoRenderer: require('./WatchCardCompactVideo'), + searchRefinementCardRenderer: require('./SearchRefinementCard'), } const keys = Reflect.ownKeys(item); From d0e363e9f27c125245ebf5250c907039fddaf17e Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Fri, 29 Apr 2022 23:29:30 +0200 Subject: [PATCH 04/55] refactor: snake_case --- lib/parser/contents/Author.js | 10 +++++----- lib/parser/contents/Channel.js | 2 +- lib/parser/contents/CollageHeroImage.js | 4 ++-- lib/parser/contents/GenericList.js | 2 +- lib/parser/contents/HorizontalList.js | 2 +- lib/parser/contents/MetadataBadge.js | 4 ++-- lib/parser/contents/Playlist.js | 2 +- lib/parser/contents/VerticalList.js | 2 +- lib/parser/contents/Video.js | 10 +++++----- lib/parser/contents/WatchCardCompactVideo.js | 2 +- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/parser/contents/Author.js b/lib/parser/contents/Author.js index cda9ddc42..56cf3b46f 100644 --- a/lib/parser/contents/Author.js +++ b/lib/parser/contents/Author.js @@ -6,7 +6,7 @@ class Author { #navText; badges; constructor(item, badges, thumbs) { - this.#navText = new NavigatableText(item); + this.#nav_text = new NavigatableText(item); this.badges = Array.isArray(badges) ? ResultsParser.parse(badges) : []; if (thumbs) { this.thumbnails = Thumbnail.fromResponse(thumbs); @@ -17,20 +17,20 @@ class Author { } get name() { - return this.#navText.text; + return this.#nav_text.text; } set name(name) { - this.#navText.text = name; + this.#nav_text.text = name; } get id() { // XXX: maybe confirm that pageType == "WEB_PAGE_TYPE_CHANNEL"? - return this.#navText.endpoint.browseId; + return this.#nav_text.endpoint.browseId; } get url() { - return this.#navText.endpoint.url; + return this.#nav_text.endpoint.url; } get isVerified() { diff --git a/lib/parser/contents/Channel.js b/lib/parser/contents/Channel.js index a3bd2b7d6..58c64be0b 100644 --- a/lib/parser/contents/Channel.js +++ b/lib/parser/contents/Channel.js @@ -12,7 +12,7 @@ class Channel { navigationEndpoint: item.navigationEndpoint }, item.ownerBadges, item.thumbnail); this.subscribers = new Text(item.subscriberCountText).text; - this.descriptionSnippet = new Text(item.descriptionSnippet).text; + this.description_snippet = new Text(item.descriptionSnippet).text; this.videos = new Text(item.videoCountText).text; } diff --git a/lib/parser/contents/CollageHeroImage.js b/lib/parser/contents/CollageHeroImage.js index 2310f8607..b861ffa2f 100644 --- a/lib/parser/contents/CollageHeroImage.js +++ b/lib/parser/contents/CollageHeroImage.js @@ -6,8 +6,8 @@ class CollageHeroImage { constructor(item) { this.left = Thumbnail.fromResponse(item.leftThumbnail); - this.topRight = Thumbnail.fromResponse(item.topRightThumbnail); - this.bottomRight = Thumbnail.fromResponse(item.bottomRightThumbnail); + this.top_right = Thumbnail.fromResponse(item.topRightThumbnail); + this.bottom_right = Thumbnail.fromResponse(item.bottomRightThumbnail); } } diff --git a/lib/parser/contents/GenericList.js b/lib/parser/contents/GenericList.js index a12ecdf59..be3afa2cd 100644 --- a/lib/parser/contents/GenericList.js +++ b/lib/parser/contents/GenericList.js @@ -4,7 +4,7 @@ const ResultsParser = require("."); module.exports = (name, field = 'contents') => { return class List { type = name; - isList = true; + is_list = true; constructor(items) { this.contents = ResultsParser.parse(items[field]); } diff --git a/lib/parser/contents/HorizontalList.js b/lib/parser/contents/HorizontalList.js index 12178884c..1b27e1a7c 100644 --- a/lib/parser/contents/HorizontalList.js +++ b/lib/parser/contents/HorizontalList.js @@ -4,7 +4,7 @@ class HorizontalList { type = 'HorizontalList'; constructor(item) { - this.visibleItemCount = item.visibleItemCount; + this.visible_item_count = item.visibleItemCount; this.items = ResultsParser.parse(item.items); } } diff --git a/lib/parser/contents/MetadataBadge.js b/lib/parser/contents/MetadataBadge.js index a9d2fa36e..ed2216337 100644 --- a/lib/parser/contents/MetadataBadge.js +++ b/lib/parser/contents/MetadataBadge.js @@ -4,12 +4,12 @@ class MetadataBadge { type = 'MetadataBadge'; style; label; - nonAbbreviatedLabel; + non_abbreviated_label; constructor(item) { this.style = item.style; this.label = item.label; - this.nonAbbreviatedLabel = item.accessibilityData?.label; + this.non_abbreviated_label = item.accessibilityData?.label; } } diff --git a/lib/parser/contents/Playlist.js b/lib/parser/contents/Playlist.js index f15dd6c6b..9a64c3e06 100644 --- a/lib/parser/contents/Playlist.js +++ b/lib/parser/contents/Playlist.js @@ -13,7 +13,7 @@ class Playlist { this.thumbnails = Array.isArray(item.thumbnails) ? item.thumbnails.map(thumbs => Thumbnail.fromResponse(thumbs)) : []; if (new Text(item.videoCountText).text !== 'Mix') this.videos = parseInt(item.videoCount); - this.firstVideos = ResultsParser.parse(item.videos); + this.first_videos = ResultsParser.parse(item.videos); } } diff --git a/lib/parser/contents/VerticalList.js b/lib/parser/contents/VerticalList.js index b350be481..13c0855ac 100644 --- a/lib/parser/contents/VerticalList.js +++ b/lib/parser/contents/VerticalList.js @@ -4,7 +4,7 @@ class VerticalList { type = 'VerticalList'; constructor(item) { - this.collapsedItemCount = item.collapsedItemCount; + this.collapsed_item_count = item.collapsedItemCount; this.items = ResultsParser.parse(item.items); } } diff --git a/lib/parser/contents/Video.js b/lib/parser/contents/Video.js index a0371f52e..938349286 100644 --- a/lib/parser/contents/Video.js +++ b/lib/parser/contents/Video.js @@ -16,7 +16,7 @@ class Video { this.title = new Text(item.title, '').text; const lengthAlt = item.thumbnailOverlays.find(overlay => overlay.hasOwnProperty('thumbnailOverlayTimeStatusRenderer'))?.thumbnailOverlayTimeStatusRenderer; this.duration = item.lengthText ? new Text(item.lengthText, '').text : lengthAlt?.text ? new Text(lengthAlt.text).text : ''; - this.publishedAt = new Text(item.publishedTimeText, '').text; + this.published_at = new Text(item.publishedTimeText, '').text; this.views = new Text(item.viewCountText, '').text; // TODO: might be simplified? this appears to only contain the description this.snippets = item?.detailedMetadataSnippets?.map(snip => ({ @@ -25,19 +25,19 @@ class Video { })); } - get isLive() { + get is_live() { return this.badges.some(badge => badge.style === 'BADGE_STYLE_TYPE_LIVE_NOW'); } - get isUpcoming() { + get is_upcoming() { return this.upcoming && this.upcoming > new Date(); } - get hasCaptions() { + get has_captions() { return this.badges.some(badge => badge.label === 'CC'); } - get bestThumbnail() { + get best_thumbnail() { return this.thumbnails[0]; } } diff --git a/lib/parser/contents/WatchCardCompactVideo.js b/lib/parser/contents/WatchCardCompactVideo.js index 3c1b9f057..030a6a207 100644 --- a/lib/parser/contents/WatchCardCompactVideo.js +++ b/lib/parser/contents/WatchCardCompactVideo.js @@ -10,7 +10,7 @@ class WatchCardCompactVideo { this.title = new Text(item.title).text; const [ views, publishedAt ] = new Text(item.subtitle).text.split('•'); this.views = views.trim(); - this.publishedAt = publishedAt?.trim(); + this.published_at = publishedAt?.trim(); this.endpoint = new NavigationEndpoint(item.navigationEndpoint); this.duration = new Text(item.lengthText).text; // TODO: byline is author? From 34d1ae7d88afda389f4d19eb2786fb8d99ac0a72 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sat, 30 Apr 2022 00:46:42 +0200 Subject: [PATCH 05/55] feat: channel home page renderers --- lib/parser/contents/Author.js | 13 +++++--- lib/parser/contents/Channel.js | 3 +- lib/parser/contents/ChannelMetadata.js | 26 +++++++++++++++ lib/parser/contents/ChannelVideoPlayer.js | 16 ++++++++++ lib/parser/contents/ChildVideo.js | 2 ++ lib/parser/contents/CollageHeroImage.js | 2 ++ lib/parser/contents/ExpandableTab.js | 15 +++++++++ lib/parser/contents/GridChannel.js | 19 +++++++++++ lib/parser/contents/GridVideo.js | 23 +++++++++++++ lib/parser/contents/NavigationEndpoint.js | 9 ++++-- lib/parser/contents/Playlist.js | 2 ++ lib/parser/contents/ReelItem.js | 18 +++++++++++ lib/parser/contents/ReelShelf.js | 15 +++++++++ lib/parser/contents/Shelf.js | 9 ++++++ lib/parser/contents/Tab.js | 15 +++++++++ lib/parser/contents/TwoColumnBrowseResults.js | 11 +++++++ lib/parser/contents/Video.js | 5 ++- lib/parser/contents/index.js | 32 ++++++++++++++++--- 18 files changed, 223 insertions(+), 12 deletions(-) create mode 100644 lib/parser/contents/ChannelMetadata.js create mode 100644 lib/parser/contents/ChannelVideoPlayer.js create mode 100644 lib/parser/contents/ExpandableTab.js create mode 100644 lib/parser/contents/GridChannel.js create mode 100644 lib/parser/contents/GridVideo.js create mode 100644 lib/parser/contents/ReelItem.js create mode 100644 lib/parser/contents/ReelShelf.js create mode 100644 lib/parser/contents/Tab.js create mode 100644 lib/parser/contents/TwoColumnBrowseResults.js diff --git a/lib/parser/contents/Author.js b/lib/parser/contents/Author.js index 56cf3b46f..d3f9db550 100644 --- a/lib/parser/contents/Author.js +++ b/lib/parser/contents/Author.js @@ -3,7 +3,7 @@ const NavigatableText = require("./NavigatableText"); const Thumbnail = require("./Thumbnail"); class Author { - #navText; + #nav_text; badges; constructor(item, badges, thumbs) { this.#nav_text = new NavigatableText(item); @@ -24,8 +24,13 @@ class Author { 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; } @@ -33,15 +38,15 @@ class Author { return this.#nav_text.endpoint.url; } - get isVerified() { + get is_verified() { return this.badges.some(badge => badge.style === 'BADGE_STYLE_TYPE_VERIFIED'); } - get isVerifiedArtist() { + get is_verified_artist() { return this.badges.some(badge => badge.style === 'BADGE_STYLE_TYPE_VERIFIED_ARTIST'); } - get bestThumbnail() { + get best_thumbnail() { return this.thumbnails[0]; } } diff --git a/lib/parser/contents/Channel.js b/lib/parser/contents/Channel.js index 58c64be0b..f5d9970e7 100644 --- a/lib/parser/contents/Channel.js +++ b/lib/parser/contents/Channel.js @@ -1,5 +1,6 @@ const ResultsParser = require("."); const Author = require("./Author"); +const NavigationEndpoint = require("./NavigationEndpoint"); const Text = require("./Text"); class Channel { @@ -14,7 +15,7 @@ class Channel { this.subscribers = new Text(item.subscriberCountText).text; this.description_snippet = new Text(item.descriptionSnippet).text; this.videos = new Text(item.videoCountText).text; - + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); } } diff --git a/lib/parser/contents/ChannelMetadata.js b/lib/parser/contents/ChannelMetadata.js new file mode 100644 index 000000000..8a47da085 --- /dev/null +++ b/lib/parser/contents/ChannelMetadata.js @@ -0,0 +1,26 @@ +const ResultsParser = require("."); +const Thumbnail = require("./Thumbnail"); + +class ChannelMetadata { + type = 'ChannelMetadata'; + + constructor(item) { + this.title = item.title; + this.description = item.description; + this.metadata = { + url: item.url, + rss_urls: item.rssUrl, + vanity_channel_url: item.vanityChannelUrl, + external_id: item.externalId, + is_family_safe: item.isFamilySafe, + keywords: item.keywords, + avatar: Thumbnail.fromResponse(item.avatar), + available_countries: item.availableCountryCodes, + android_deep_link: item.androidDeepLink, + android_appindexing_link: item.androidAppIndexingLink, + ios_appindexing_link: item.iosAppIndexingLink, + } + } +} + +module.exports = ChannelMetadata; \ No newline at end of file diff --git a/lib/parser/contents/ChannelVideoPlayer.js b/lib/parser/contents/ChannelVideoPlayer.js new file mode 100644 index 000000000..57e2e0f24 --- /dev/null +++ b/lib/parser/contents/ChannelVideoPlayer.js @@ -0,0 +1,16 @@ +const ResultsParser = require("."); +const Text = require("./Text"); + +class ChannelVideoPlayer { + type = 'ChannelVideoPlayer'; + + constructor(item) { + this.id = item.videoId; + this.title = new Text(item.title, '').text; + this.description = new Text(item.description, '').text; + this.views = new Text(item.viewCountText, '').text; + this.published_at = new Text(item.publishedTimeText, '').text; + } +} + +module.exports = ChannelVideoPlayer; \ No newline at end of file diff --git a/lib/parser/contents/ChildVideo.js b/lib/parser/contents/ChildVideo.js index 14f22c34c..1b50a8a40 100644 --- a/lib/parser/contents/ChildVideo.js +++ b/lib/parser/contents/ChildVideo.js @@ -1,4 +1,5 @@ const ResultsParser = require("."); +const NavigationEndpoint = require("./NavigationEndpoint"); const Text = require("./Text"); class ChildVideo { @@ -8,6 +9,7 @@ class ChildVideo { this.id = item.videoId; this.title = new Text(item.title).text; this.length = new Text(item.lengthText).text; + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); } } diff --git a/lib/parser/contents/CollageHeroImage.js b/lib/parser/contents/CollageHeroImage.js index b861ffa2f..cb3063fb6 100644 --- a/lib/parser/contents/CollageHeroImage.js +++ b/lib/parser/contents/CollageHeroImage.js @@ -1,4 +1,5 @@ const ResultsParser = require("."); +const NavigationEndpoint = require("./NavigationEndpoint"); const Thumbnail = require("./Thumbnail"); class CollageHeroImage { @@ -8,6 +9,7 @@ class CollageHeroImage { this.left = Thumbnail.fromResponse(item.leftThumbnail); this.top_right = Thumbnail.fromResponse(item.topRightThumbnail); this.bottom_right = Thumbnail.fromResponse(item.bottomRightThumbnail); + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); } } diff --git a/lib/parser/contents/ExpandableTab.js b/lib/parser/contents/ExpandableTab.js new file mode 100644 index 000000000..1da4d98d6 --- /dev/null +++ b/lib/parser/contents/ExpandableTab.js @@ -0,0 +1,15 @@ +const ResultsParser = require("."); +const NavigationEndpoint = require("./NavigationEndpoint"); + +class ExpandableTab { + type = 'ExpandableTab'; + + constructor(item) { + this.title = item.title; + this.endpoint = new NavigationEndpoint(item.endpoint); + this.selected = item.selected; // if this.selected then we may have content else we do not + this.content = item.content ? ResultsParser.parseItem(item.content) : null; + } +} + +module.exports = ExpandableTab; \ No newline at end of file diff --git a/lib/parser/contents/GridChannel.js b/lib/parser/contents/GridChannel.js new file mode 100644 index 000000000..144a67e7e --- /dev/null +++ b/lib/parser/contents/GridChannel.js @@ -0,0 +1,19 @@ +const ResultsParser = require("."); +const NavigationEndpoint = require("./NavigationEndpoint"); +const Text = require("./Text"); +const Thumbnail = require("./Thumbnail"); + +class GridChannel { + type = 'GridChannel'; + + constructor(item) { + this.id = item.channelId; + this.thumbnails = Thumbnail.fromResponse(item.thumbnail); + this.videos = new Text(item.videoCountText).text; + this.subscribers = new Text(item.subscriberCountText).text; + this.name = new Text(item.title).text; + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + } +} + +module.exports = GridChannel; \ No newline at end of file diff --git a/lib/parser/contents/GridVideo.js b/lib/parser/contents/GridVideo.js new file mode 100644 index 000000000..8fbf78e32 --- /dev/null +++ b/lib/parser/contents/GridVideo.js @@ -0,0 +1,23 @@ +const ResultsParser = require("."); +const NavigationEndpoint = require("./NavigationEndpoint"); +const Text = require("./Text"); +const Thumbnail = require("./Thumbnail"); + +class GridVideo { + type = 'GridVideo'; + + constructor(item) { + this.id = item.videoId; + this.thumbnails = Thumbnail.fromResponse(item.thumbnail); + this.title = new Text(item.title, '').text; + this.badges = Array.isArray(item.badges) ? ResultsParser.parse(item.badges) : []; + const lengthAlt = item.thumbnailOverlays.find(overlay => overlay.hasOwnProperty('thumbnailOverlayTimeStatusRenderer'))?.thumbnailOverlayTimeStatusRenderer; + this.duration = item.lengthText ? new Text(item.lengthText, '').text : lengthAlt?.text ? new Text(lengthAlt.text).text : ''; + this.published_at = new Text(item.publishedTimeText, '').text; + this.views = new Text(item.viewCountText, '').text; + // TODO: rich thumbnail? + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + } +} + +module.exports = GridVideo; \ No newline at end of file diff --git a/lib/parser/contents/NavigationEndpoint.js b/lib/parser/contents/NavigationEndpoint.js index 3bab8800b..336a0cdf3 100644 --- a/lib/parser/contents/NavigationEndpoint.js +++ b/lib/parser/contents/NavigationEndpoint.js @@ -7,14 +7,19 @@ class NavigationEndpoint { item.browseEndpoint?.canonicalBaseUrl || item.commandMetadata.webCommandMetadata.url; // TODO: clean this up! - // browseId is most likely the channel id - this.browseId = item.browseEndpoint?.browseId; + this.browse = item.browseEndpoint ? { + browseId: item.browseEndpoint.browseId, + params: item.browseEndpoint.params, + base_url: item.browseEndpoint.canonicalBaseUrl + } : null; // this is the video id to navigate to this.watchVideoId = item.watchEndpoint?.videoId; // this is a playlist page to navigate to // but redirect and actually start playing it // see url for index (playnext and index searchParams) this.watchPlaylistId = item.watchPlaylistEndpoint?.playlistId; + // reels have their own navigation endpoint for some reason + this.watchReelId = item.reelWatchEndpoint?.videoId; this.pageType = item.commandMetadata.webCommandMetadata.webPageType || 'UNKNOWN'; } } diff --git a/lib/parser/contents/Playlist.js b/lib/parser/contents/Playlist.js index 9a64c3e06..593326636 100644 --- a/lib/parser/contents/Playlist.js +++ b/lib/parser/contents/Playlist.js @@ -1,5 +1,6 @@ const ResultsParser = require("."); const Author = require("./Author"); +const NavigationEndpoint = require("./NavigationEndpoint"); const Text = require("./Text"); const Thumbnail = require("./Thumbnail"); @@ -14,6 +15,7 @@ class Playlist { if (new Text(item.videoCountText).text !== 'Mix') this.videos = parseInt(item.videoCount); this.first_videos = ResultsParser.parse(item.videos); + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); } } diff --git a/lib/parser/contents/ReelItem.js b/lib/parser/contents/ReelItem.js new file mode 100644 index 000000000..973663da8 --- /dev/null +++ b/lib/parser/contents/ReelItem.js @@ -0,0 +1,18 @@ +const ResultsParser = require("."); +const NavigationEndpoint = require("./NavigationEndpoint"); +const Text = require("./Text"); +const Thumbnail = require("./Thumbnail"); + +class ReelItem { + type = 'ReelItem'; + + constructor(item) { + this.id = item.videoId; + this.title = new Text(item.headline, '').text; + this.thumbnails = Thumbnail.fromResponse(item.thumbnail); + this.views = new Text(item.viewCountText, '').text; + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + } +} + +module.exports = ReelItem; \ No newline at end of file diff --git a/lib/parser/contents/ReelShelf.js b/lib/parser/contents/ReelShelf.js new file mode 100644 index 000000000..b696ea3e1 --- /dev/null +++ b/lib/parser/contents/ReelShelf.js @@ -0,0 +1,15 @@ +const ResultsParser = require("."); +const NavigationEndpoint = require("./NavigationEndpoint"); +const Text = require("./Text"); + +class ReelShelf { + type = 'ReelShelf'; + + constructor(item) { + this.title = new Text(item.title).text; + this.items = ResultsParser.parse(item.items); + this.endpoint = item.endpoint ? new NavigationEndpoint(item.endpoint) : null; + } +} + +module.exports = ReelShelf; \ No newline at end of file diff --git a/lib/parser/contents/Shelf.js b/lib/parser/contents/Shelf.js index e43d2f4b7..a5b60c703 100644 --- a/lib/parser/contents/Shelf.js +++ b/lib/parser/contents/Shelf.js @@ -1,4 +1,5 @@ const ResultsParser = require("."); +const NavigationEndpoint = require("./NavigationEndpoint"); const Text = require("./Text"); class Shelf { @@ -7,6 +8,14 @@ class Shelf { constructor(item) { this.title = new Text(item.title).text; this.content = ResultsParser.parseItem(item.content); + this.endpoint = item.endpoint ? new NavigationEndpoint(item.endpoint) : null; + // XXX: maybe add this as buttonRenderer? + // this is the playAllButton in the original response + this.button = item.playAllButton?.buttonRenderer ? { + text: new Text(item.playAllButton.buttonRenderer.text, '').text, + endpoint: item.playAllButton.buttonRenderer.navigationEndpoint ? new NavigationEndpoint(item.playAllButton.buttonRenderer.navigationEndpoint) : null, + icon: item.playAllButton.buttonRenderer.icon?.iconType || 'UNKNOWN' + } : null; } } diff --git a/lib/parser/contents/Tab.js b/lib/parser/contents/Tab.js new file mode 100644 index 000000000..255a28594 --- /dev/null +++ b/lib/parser/contents/Tab.js @@ -0,0 +1,15 @@ +const ResultsParser = require("."); +const NavigationEndpoint = require("./NavigationEndpoint"); + +class Tab { + type = 'Tab'; + + constructor(item) { + this.title = item.title; + this.endpoint = new NavigationEndpoint(item.endpoint); + this.selected = item.selected; // if this.selected then we may have content else we do not + this.content = item.content ? ResultsParser.parseItem(item.content) : null; + } +} + +module.exports = Tab; \ No newline at end of file diff --git a/lib/parser/contents/TwoColumnBrowseResults.js b/lib/parser/contents/TwoColumnBrowseResults.js new file mode 100644 index 000000000..8f5fd19f5 --- /dev/null +++ b/lib/parser/contents/TwoColumnBrowseResults.js @@ -0,0 +1,11 @@ +const ResultsParser = require("."); + +class TwoColumnBrowseResults { + type = 'TwoColumnBrowseResults'; + + constructor(item) { + this.tabs = ResultsParser.parse(item.tabs); + } +} + +module.exports = TwoColumnBrowseResults; \ No newline at end of file diff --git a/lib/parser/contents/Video.js b/lib/parser/contents/Video.js index 938349286..59ccb93e9 100644 --- a/lib/parser/contents/Video.js +++ b/lib/parser/contents/Video.js @@ -1,5 +1,6 @@ const ResultsParser = require("."); const Author = require("./Author"); +const NavigationEndpoint = require("./NavigationEndpoint"); const Text = require("./Text"); const Thumbnail = require("./Thumbnail"); @@ -9,7 +10,8 @@ class Video { constructor(item) { this.author = new Author(item.ownerText, item.ownerBadges, item.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail); this.badges = Array.isArray(item.badges) ? ResultsParser.parse(item.badges) : []; - this.thumbnails = Thumbnail.fromResponse(item); + // TODO: verify this works + this.thumbnails = Thumbnail.fromResponse(item.thumbnail); const upcoming = item.upcomingEventData ? Number(`${item.upcomingEventData.startTime}000`) : null; if (upcoming) this.upcoming = new Date(upcoming); this.id = item.videoId; @@ -23,6 +25,7 @@ class Video { text: new Text(snip.snippetText, '').text, hoverText: new Text(snip.snippetHoverText, '').text, })); + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); } get is_live() { diff --git a/lib/parser/contents/index.js b/lib/parser/contents/index.js index 53a7b6b3e..f8378d3b6 100644 --- a/lib/parser/contents/index.js +++ b/lib/parser/contents/index.js @@ -1,11 +1,30 @@ class ResultsParser { - static parse(results) { - return results.map((item) => this.parseItem(item)).filter((item) => item); + static parseSearch(results) { + return { + contents: ResultsParser.parse(results.contents), + refinements: results.refinements || [], + estimated_results: parseInt(results.estimated_results), + } + } + + static parseChannel(results) { + return { + header: ResultsParser.parseItem(results.header), + metadata: ResultsParser.parseItem(results.metadata), + contents: ResultsParser.parse(results.contents), + } + } + + static parse(contents) { + return contents.map((item) => this.parseItem(item)).filter((item) => item); } static parseItem(item) { const renderers = { twoColumnSearchResultsRenderer: require('./TwoColumnSearchResults'), + twoColumnBrowseResultsRenderer: require('./TwoColumnBrowseResults'), + tabRenderer: require('./Tab'), + expandableTabRenderer: require('./ExpandableTab'), videoRenderer: require('./Video'), metadataBadgeRenderer: require('./MetadataBadge'), channelRenderer: require('./Channel'), @@ -17,7 +36,7 @@ class ResultsParser { horizontalListRenderer: require('./HorizontalList'), sectionListRenderer: require('./GenericList')('SectionList'), secondarySearchContainerRenderer: require('./GenericList')('SecondarySearchContainer'), - itemSectionRenderer: require('./GenericList')('SecondarySearchContainer'), + itemSectionRenderer: require('./GenericList')('ItemSection'), universalWatchCardRenderer: require('./UniversalWatchCard'), watchCardRichHeaderRenderer: require('./WatchCardRichHeader'), watchCardHeroVideoRenderer: require('./WatchCardHeroVideo'), @@ -27,6 +46,11 @@ class ResultsParser { horizontalCardListRenderer: require('./HorizontalCardList'), watchCardCompactVideoRenderer: require('./WatchCardCompactVideo'), searchRefinementCardRenderer: require('./SearchRefinementCard'), + channelVideoPlayerRenderer: require('./ChannelVideoPlayer'), + gridVideoRenderer: require('./GridVideo'), + gridChannelRenderer: require('./GridChannel'), + reelShelfRenderer: require('./ReelShelf'), + reelItemRenderer: require('./ReelItem'), } const keys = Reflect.ownKeys(item); @@ -38,7 +62,7 @@ class ResultsParser { } const result = new renderers[keys[0]](item[keys[0]]); - if (keys[0] === 'shelfRenderer') + if (keys[0] === 'gridChannelRenderer') console.log(result); return result; } From 163e58cf2bc30b5bebd0487d32fec7827c573da4 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sat, 30 Apr 2022 13:26:29 +0200 Subject: [PATCH 06/55] feat: parsers for more channel tabs These are needed for channel tabs: Videos, Playlists, Community, Channels Additionally, do not merely return text as string, since they may include links which may be navigated to --- lib/parser/contents/BackstageImage.js | 12 +++++++ lib/parser/contents/BackstagePost.js | 26 ++++++++++++++ lib/parser/contents/BackstagePostThread.js | 11 ++++++ lib/parser/contents/Button.js | 15 ++++++++ lib/parser/contents/Channel.js | 6 ++-- lib/parser/contents/ChannelVideoPlayer.js | 8 ++--- lib/parser/contents/ChildVideo.js | 4 +-- lib/parser/contents/CommentActionButtons.js | 13 +++++++ lib/parser/contents/ContinuationItem.js | 12 +++++++ lib/parser/contents/GridChannel.js | 6 ++-- lib/parser/contents/GridPlaylist.js | 19 ++++++++++ lib/parser/contents/GridVideo.js | 8 ++--- lib/parser/contents/HorizontalCardList.js | 2 +- lib/parser/contents/NavigationEndpoint.js | 35 ++++++++++++++---- lib/parser/contents/Playlist.js | 4 +-- lib/parser/contents/ReelItem.js | 4 +-- lib/parser/contents/ReelShelf.js | 2 +- lib/parser/contents/SearchRefinementCard.js | 2 +- lib/parser/contents/Shelf.js | 4 +-- lib/parser/contents/Text.js | 22 +++++++++++- lib/parser/contents/ToggleButton.js | 38 ++++++++++++++++++++ lib/parser/contents/Video.js | 12 +++---- lib/parser/contents/WatchCardCompactVideo.js | 6 ++-- lib/parser/contents/WatchCardRichHeader.js | 4 +-- lib/parser/contents/index.js | 9 +++++ 25 files changed, 241 insertions(+), 43 deletions(-) create mode 100644 lib/parser/contents/BackstageImage.js create mode 100644 lib/parser/contents/BackstagePost.js create mode 100644 lib/parser/contents/BackstagePostThread.js create mode 100644 lib/parser/contents/Button.js create mode 100644 lib/parser/contents/CommentActionButtons.js create mode 100644 lib/parser/contents/ContinuationItem.js create mode 100644 lib/parser/contents/GridPlaylist.js create mode 100644 lib/parser/contents/ToggleButton.js diff --git a/lib/parser/contents/BackstageImage.js b/lib/parser/contents/BackstageImage.js new file mode 100644 index 000000000..7967e0bfc --- /dev/null +++ b/lib/parser/contents/BackstageImage.js @@ -0,0 +1,12 @@ +const ResultsParser = require("."); +const Thumbnail = require("./Thumbnail"); + +class BackstageImage { + type = 'BackstageImage'; + + constructor(item) { + this.image = Thumbnail.fromResponse(item.image); + } +} + +module.exports = BackstageImage; \ No newline at end of file diff --git a/lib/parser/contents/BackstagePost.js b/lib/parser/contents/BackstagePost.js new file mode 100644 index 000000000..be9b2044b --- /dev/null +++ b/lib/parser/contents/BackstagePost.js @@ -0,0 +1,26 @@ +const ResultsParser = require("."); +const Author = require("./Author"); +const Text = require("./Text"); + +class BackstagePost { + type = 'BackstagePost'; + + constructor(item) { + this.id = item.postId; + this.author = new Author({ + ...item.authorText, + navigationEndpoint: item.authorEndpoint + }, null, item.authorThumbnail); + this.content = new Text(item.contentText, ''); + this.published_at = new Text(item.publishedTimeText); + this.likes = new Text(item.voteCount); + this.actions = ResultsParser.parseItem(item.actionButtons); + this.attachment = item.backstageAttachment ? ResultsParser.parseItem(item.backstageAttachment) : null; + } + + get endpoint() { + return this.actions.reply.endpoint; + } +} + +module.exports = BackstagePost; \ No newline at end of file diff --git a/lib/parser/contents/BackstagePostThread.js b/lib/parser/contents/BackstagePostThread.js new file mode 100644 index 000000000..2d9d4c112 --- /dev/null +++ b/lib/parser/contents/BackstagePostThread.js @@ -0,0 +1,11 @@ +const ResultsParser = require("."); + +class BackstagePostThread { + type = 'BackstagePostThread'; + + constructor(item) { + this.post = ResultsParser.parseItem(item.post); + } +} + +module.exports = BackstagePostThread; \ No newline at end of file diff --git a/lib/parser/contents/Button.js b/lib/parser/contents/Button.js new file mode 100644 index 000000000..c811526b9 --- /dev/null +++ b/lib/parser/contents/Button.js @@ -0,0 +1,15 @@ +const ResultsParser = require("."); +const NavigationEndpoint = require("./NavigationEndpoint"); +const Text = require("./Text"); + +class Button { + type = 'Button'; + + constructor(item) { + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + this.text = new Text(item.text); + this.tooltip = item.tooltip; + } +} + +module.exports = Button; \ No newline at end of file diff --git a/lib/parser/contents/Channel.js b/lib/parser/contents/Channel.js index f5d9970e7..582f753df 100644 --- a/lib/parser/contents/Channel.js +++ b/lib/parser/contents/Channel.js @@ -12,9 +12,9 @@ class Channel { ...item.title, navigationEndpoint: item.navigationEndpoint }, item.ownerBadges, item.thumbnail); - this.subscribers = new Text(item.subscriberCountText).text; - this.description_snippet = new Text(item.descriptionSnippet).text; - this.videos = new Text(item.videoCountText).text; + this.subscribers = new Text(item.subscriberCountText); + this.description_snippet = new Text(item.descriptionSnippet); + this.videos = new Text(item.videoCountText); this.endpoint = new NavigationEndpoint(item.navigationEndpoint); } } diff --git a/lib/parser/contents/ChannelVideoPlayer.js b/lib/parser/contents/ChannelVideoPlayer.js index 57e2e0f24..2eaa75b9c 100644 --- a/lib/parser/contents/ChannelVideoPlayer.js +++ b/lib/parser/contents/ChannelVideoPlayer.js @@ -6,10 +6,10 @@ class ChannelVideoPlayer { constructor(item) { this.id = item.videoId; - this.title = new Text(item.title, '').text; - this.description = new Text(item.description, '').text; - this.views = new Text(item.viewCountText, '').text; - this.published_at = new Text(item.publishedTimeText, '').text; + this.title = new Text(item.title, ''); + this.description = new Text(item.description, ''); + this.views = new Text(item.viewCountText, ''); + this.published_at = new Text(item.publishedTimeText, ''); } } diff --git a/lib/parser/contents/ChildVideo.js b/lib/parser/contents/ChildVideo.js index 1b50a8a40..c189fd3d2 100644 --- a/lib/parser/contents/ChildVideo.js +++ b/lib/parser/contents/ChildVideo.js @@ -7,8 +7,8 @@ class ChildVideo { constructor(item) { this.id = item.videoId; - this.title = new Text(item.title).text; - this.length = new Text(item.lengthText).text; + this.title = new Text(item.title); + this.length = new Text(item.lengthText); this.endpoint = new NavigationEndpoint(item.navigationEndpoint); } } diff --git a/lib/parser/contents/CommentActionButtons.js b/lib/parser/contents/CommentActionButtons.js new file mode 100644 index 000000000..56356ba3b --- /dev/null +++ b/lib/parser/contents/CommentActionButtons.js @@ -0,0 +1,13 @@ +const ResultsParser = require("."); + +class CommentActionButtons { + type = 'CommentActionButtons'; + + constructor(item) { + this.like = ResultsParser.parseItem(item.likeButton); + this.reply = ResultsParser.parseItem(item.replyButton); + this.dislike = ResultsParser.parseItem(item.dislikeButton); + } +} + +module.exports = CommentActionButtons; \ No newline at end of file diff --git a/lib/parser/contents/ContinuationItem.js b/lib/parser/contents/ContinuationItem.js new file mode 100644 index 000000000..978503be7 --- /dev/null +++ b/lib/parser/contents/ContinuationItem.js @@ -0,0 +1,12 @@ +const ResultsParser = require("."); +const NavigationEndpoint = require("./NavigationEndpoint"); + +class ContinuationItem { + type = 'ContinuationItem'; + + constructor(item) { + this.endpoint = new NavigationEndpoint(item.continuationEndpoint); + } +} + +module.exports = ContinuationItem; \ No newline at end of file diff --git a/lib/parser/contents/GridChannel.js b/lib/parser/contents/GridChannel.js index 144a67e7e..6d266a773 100644 --- a/lib/parser/contents/GridChannel.js +++ b/lib/parser/contents/GridChannel.js @@ -9,9 +9,9 @@ class GridChannel { constructor(item) { this.id = item.channelId; this.thumbnails = Thumbnail.fromResponse(item.thumbnail); - this.videos = new Text(item.videoCountText).text; - this.subscribers = new Text(item.subscriberCountText).text; - this.name = new Text(item.title).text; + this.videos = new Text(item.videoCountText); + this.subscribers = new Text(item.subscriberCountText); + this.name = new Text(item.title); this.endpoint = new NavigationEndpoint(item.navigationEndpoint); } } diff --git a/lib/parser/contents/GridPlaylist.js b/lib/parser/contents/GridPlaylist.js new file mode 100644 index 000000000..e70dfe56c --- /dev/null +++ b/lib/parser/contents/GridPlaylist.js @@ -0,0 +1,19 @@ +const ResultsParser = require("."); +const NavigationEndpoint = require("./NavigationEndpoint"); +const Text = require("./Text"); +const Thumbnail = require("./Thumbnail"); + +class GridPlaylist { + type = 'GridPlaylist'; + + constructor(item) { + this.id = item.playlistId; + this.videos = new Text(item.videoCountShortText); + this.thumbnauls = Thumbnail.fromResponse(item.thumbnail); + this.video_thumbnails = Array.isArray(item.sidebarThumbnails) ? item.sidebarThumbnails.map(thumbs => Thumbnail.fromResponse(thumbs)) : []; + this.title = new Text(item.title); + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + } +} + +module.exports = GridPlaylist; \ No newline at end of file diff --git a/lib/parser/contents/GridVideo.js b/lib/parser/contents/GridVideo.js index 8fbf78e32..ce731b3a3 100644 --- a/lib/parser/contents/GridVideo.js +++ b/lib/parser/contents/GridVideo.js @@ -9,12 +9,12 @@ class GridVideo { constructor(item) { this.id = item.videoId; this.thumbnails = Thumbnail.fromResponse(item.thumbnail); - this.title = new Text(item.title, '').text; + this.title = new Text(item.title, ''); this.badges = Array.isArray(item.badges) ? ResultsParser.parse(item.badges) : []; const lengthAlt = item.thumbnailOverlays.find(overlay => overlay.hasOwnProperty('thumbnailOverlayTimeStatusRenderer'))?.thumbnailOverlayTimeStatusRenderer; - this.duration = item.lengthText ? new Text(item.lengthText, '').text : lengthAlt?.text ? new Text(lengthAlt.text).text : ''; - this.published_at = new Text(item.publishedTimeText, '').text; - this.views = new Text(item.viewCountText, '').text; + this.duration = item.lengthText ? new Text(item.lengthText, '') : lengthAlt?.text ? new Text(lengthAlt.text) : ''; + this.published_at = new Text(item.publishedTimeText, ''); + this.views = new Text(item.viewCountText, ''); // TODO: rich thumbnail? this.endpoint = new NavigationEndpoint(item.navigationEndpoint); } diff --git a/lib/parser/contents/HorizontalCardList.js b/lib/parser/contents/HorizontalCardList.js index 3a5c34b2e..31daee505 100644 --- a/lib/parser/contents/HorizontalCardList.js +++ b/lib/parser/contents/HorizontalCardList.js @@ -6,7 +6,7 @@ class HorizontalCardList { constructor(item) { this.cards = ResultsParser.parse(item.cards); - this.header = item.header?.title ? new Text(item.header).text : null; + this.header = item.header?.title ? new Text(item.header) : null; } } diff --git a/lib/parser/contents/NavigationEndpoint.js b/lib/parser/contents/NavigationEndpoint.js index 336a0cdf3..0874f99d0 100644 --- a/lib/parser/contents/NavigationEndpoint.js +++ b/lib/parser/contents/NavigationEndpoint.js @@ -1,11 +1,15 @@ class NavigationEndpoint { type = 'NavigationEndpoint'; url; - browseId; constructor (item) { - this.url = - item.browseEndpoint?.canonicalBaseUrl || - item.commandMetadata.webCommandMetadata.url; + // TODO: safely remove this: + this.url = item.commandMetadata.webCommandMetadata.url; + this.metadata = { + api_url: item.commandMetadata.webCommandMetadata.api_url, + url: item.commandMetadata.webCommandMetadata.url, + send_post: item.commandMetadata.webCommandMetadata.sendPost, + page_type: item.commandMetadata.webCommandMetadata.webPageType, + } // TODO: clean this up! this.browse = item.browseEndpoint ? { browseId: item.browseEndpoint.browseId, @@ -13,14 +17,33 @@ class NavigationEndpoint { base_url: item.browseEndpoint.canonicalBaseUrl } : null; // this is the video id to navigate to - this.watchVideoId = item.watchEndpoint?.videoId; + this.watchVideoId = item.watchEndpoint ? { + videoId: item.watchEndpoint.videoId, + playlistId: item.watchEndpoint.playlistId + } : null; + // this is a search button + this.search = item.searchEndpoint ? { + query: item.searchEndpoint.query, + params: item.searchEndpoint.params || null, + } : null; // this is a playlist page to navigate to // but redirect and actually start playing it // see url for index (playnext and index searchParams) this.watchPlaylistId = item.watchPlaylistEndpoint?.playlistId; // reels have their own navigation endpoint for some reason this.watchReelId = item.reelWatchEndpoint?.videoId; - this.pageType = item.commandMetadata.webCommandMetadata.webPageType || 'UNKNOWN'; + // external url + this.url = item.urlEndpoint ? { + url: new URL(item.urlEndpoint.url), + target: item.urlEndpoint.target, + nofollow: item.urlEndpoint.nofollow || false + } : null; + // continuations + this.continuation = item.continuationCommand ? { + request: item.continuationCommand.request, + token: item.continuationCommand.token, + trigger: item.trigger // TODO: is this the right place for this? + } : null; } } diff --git a/lib/parser/contents/Playlist.js b/lib/parser/contents/Playlist.js index 593326636..9f61fb81a 100644 --- a/lib/parser/contents/Playlist.js +++ b/lib/parser/contents/Playlist.js @@ -9,10 +9,10 @@ class Playlist { constructor(item) { this.id = item.playlistId; - this.title = new Text(item.title).text; + this.title = new Text(item.title); this.author = item.longBylineText.simpleText ? null : new Author(item.longBylineText, item.ownerBadges); this.thumbnails = Array.isArray(item.thumbnails) ? item.thumbnails.map(thumbs => Thumbnail.fromResponse(thumbs)) : []; - if (new Text(item.videoCountText).text !== 'Mix') + if (new Text(item.videoCountText) !== 'Mix') this.videos = parseInt(item.videoCount); this.first_videos = ResultsParser.parse(item.videos); this.endpoint = new NavigationEndpoint(item.navigationEndpoint); diff --git a/lib/parser/contents/ReelItem.js b/lib/parser/contents/ReelItem.js index 973663da8..cdd07d94c 100644 --- a/lib/parser/contents/ReelItem.js +++ b/lib/parser/contents/ReelItem.js @@ -8,9 +8,9 @@ class ReelItem { constructor(item) { this.id = item.videoId; - this.title = new Text(item.headline, '').text; + this.title = new Text(item.headline, ''); this.thumbnails = Thumbnail.fromResponse(item.thumbnail); - this.views = new Text(item.viewCountText, '').text; + this.views = new Text(item.viewCountText, ''); this.endpoint = new NavigationEndpoint(item.navigationEndpoint); } } diff --git a/lib/parser/contents/ReelShelf.js b/lib/parser/contents/ReelShelf.js index b696ea3e1..fd1c9f861 100644 --- a/lib/parser/contents/ReelShelf.js +++ b/lib/parser/contents/ReelShelf.js @@ -6,7 +6,7 @@ class ReelShelf { type = 'ReelShelf'; constructor(item) { - this.title = new Text(item.title).text; + this.title = new Text(item.title); this.items = ResultsParser.parse(item.items); this.endpoint = item.endpoint ? new NavigationEndpoint(item.endpoint) : null; } diff --git a/lib/parser/contents/SearchRefinementCard.js b/lib/parser/contents/SearchRefinementCard.js index 981a304d9..21d0b1196 100644 --- a/lib/parser/contents/SearchRefinementCard.js +++ b/lib/parser/contents/SearchRefinementCard.js @@ -9,7 +9,7 @@ class SearchRefinementCard { constructor(item) { this.thumbnail = Thumbnail.fromResponse(item.thumbnail); this.endpoint = new NavigationEndpoint(item.searchEndpoint); - this.query = new Text(item.query).text; + this.query = new Text(item.query); // XXX: is this actually useful? this.style = item.searchRefinementCardRendererStyle?.value; } diff --git a/lib/parser/contents/Shelf.js b/lib/parser/contents/Shelf.js index a5b60c703..7e0c1ef2e 100644 --- a/lib/parser/contents/Shelf.js +++ b/lib/parser/contents/Shelf.js @@ -6,13 +6,13 @@ class Shelf { type = 'Shelf'; constructor(item) { - this.title = new Text(item.title).text; + this.title = new Text(item.title); this.content = ResultsParser.parseItem(item.content); this.endpoint = item.endpoint ? new NavigationEndpoint(item.endpoint) : null; // XXX: maybe add this as buttonRenderer? // this is the playAllButton in the original response this.button = item.playAllButton?.buttonRenderer ? { - text: new Text(item.playAllButton.buttonRenderer.text, '').text, + text: new Text(item.playAllButton.buttonRenderer.text, ''), endpoint: item.playAllButton.buttonRenderer.navigationEndpoint ? new NavigationEndpoint(item.playAllButton.buttonRenderer.navigationEndpoint) : null, icon: item.playAllButton.buttonRenderer.icon?.iconType || 'UNKNOWN' } : null; diff --git a/lib/parser/contents/Text.js b/lib/parser/contents/Text.js index 8f84fee15..46e05428d 100644 --- a/lib/parser/contents/Text.js +++ b/lib/parser/contents/Text.js @@ -1,3 +1,5 @@ +const NavigationEndpoint = require("./NavigationEndpoint"); + class Text { type = 'Text'; text; @@ -9,6 +11,7 @@ class Text { this.text = txt.simpleText; else if (Array.isArray(txt.runs)) { this.text = txt.runs.map(a => a.text).join(''); + this.runs = txt.runs.map(a => new TextRun(a)); } else this.text = def; } @@ -18,7 +21,24 @@ class Text { } toJSON() { - return this.text; + return { + text: this.text, + runs: this.runs || [ + { + text: this.text + } + ] + }; + } +} + +class TextRun { + type = 'TextRun'; + text; + endpoint; + constructor(node) { + this.text = node.text; + this.endpoint = node.navigationEndpoint ? new NavigationEndpoint(node.navigationEndpoint) : null; } } diff --git a/lib/parser/contents/ToggleButton.js b/lib/parser/contents/ToggleButton.js new file mode 100644 index 000000000..3124972f6 --- /dev/null +++ b/lib/parser/contents/ToggleButton.js @@ -0,0 +1,38 @@ +const ResultsParser = require("."); +const NavigationEndpoint = require("./NavigationEndpoint"); + +class ToggleButton { + type = 'ToggleButton'; + + constructor(item) { + this.is_toggled = item.isToggled; + this.is_disabled = item.isDisabled; + this.default_service_endpoint = item.defaultServiceEndpoint && new NavigationEndpoint(item.defaultServiceEndpoint); + this.toggled_service_endpoint = item.toggledServiceEndpoint && new NavigationEndpoint(item.toggledServiceEndpoint); + this.default_navigation_endpoint = item.defaultNavigationEndpoint && new NavigationEndpoint(item.defaultNavigationEndpoint); + this.default_tooltip = item.defaultTooltip; + this.toggled_tooltip = item.toggledTooltip; + } + + get endpoint() { + if (this.default_navigation_endpoint) { + return this.default_navigation_endpoint; + } + if (this.is_toggled && this.toggled_service_endpoint) { + return this.toggled_service_endpoint; + } + if (!this.is_toggled && this.default_service_endpoint) { + return this.default_service_endpoint; + } + return null; + } + + get tooltip() { + if (this.is_toggled) { + return this.toggled_tooltip; + } + return this.default_tooltip; + } +} + +module.exports = ToggleButton; \ No newline at end of file diff --git a/lib/parser/contents/Video.js b/lib/parser/contents/Video.js index 59ccb93e9..9cbb2cef3 100644 --- a/lib/parser/contents/Video.js +++ b/lib/parser/contents/Video.js @@ -15,15 +15,15 @@ class Video { const upcoming = item.upcomingEventData ? Number(`${item.upcomingEventData.startTime}000`) : null; if (upcoming) this.upcoming = new Date(upcoming); this.id = item.videoId; - this.title = new Text(item.title, '').text; + this.title = new Text(item.title, ''); const lengthAlt = item.thumbnailOverlays.find(overlay => overlay.hasOwnProperty('thumbnailOverlayTimeStatusRenderer'))?.thumbnailOverlayTimeStatusRenderer; - this.duration = item.lengthText ? new Text(item.lengthText, '').text : lengthAlt?.text ? new Text(lengthAlt.text).text : ''; - this.published_at = new Text(item.publishedTimeText, '').text; - this.views = new Text(item.viewCountText, '').text; + this.duration = item.lengthText ? new Text(item.lengthText, '') : lengthAlt?.text ? new Text(lengthAlt.text) : ''; + this.published_at = new Text(item.publishedTimeText, ''); + this.views = new Text(item.viewCountText, ''); // TODO: might be simplified? this appears to only contain the description this.snippets = item?.detailedMetadataSnippets?.map(snip => ({ - text: new Text(snip.snippetText, '').text, - hoverText: new Text(snip.snippetHoverText, '').text, + text: new Text(snip.snippetText, ''), + hoverText: new Text(snip.snippetHoverText, ''), })); this.endpoint = new NavigationEndpoint(item.navigationEndpoint); } diff --git a/lib/parser/contents/WatchCardCompactVideo.js b/lib/parser/contents/WatchCardCompactVideo.js index 030a6a207..be53ce183 100644 --- a/lib/parser/contents/WatchCardCompactVideo.js +++ b/lib/parser/contents/WatchCardCompactVideo.js @@ -7,12 +7,12 @@ class WatchCardCompactVideo { type = 'WatchCardCompactVideo'; constructor(item) { - this.title = new Text(item.title).text; - const [ views, publishedAt ] = new Text(item.subtitle).text.split('•'); + this.title = new Text(item.title); + const [ views, publishedAt ] = new Text(item.subtitle).split('•'); this.views = views.trim(); this.published_at = publishedAt?.trim(); this.endpoint = new NavigationEndpoint(item.navigationEndpoint); - this.duration = new Text(item.lengthText).text; + this.duration = new Text(item.lengthText); // TODO: byline is author? this.byline = new NavigatableText(item.byline); } diff --git a/lib/parser/contents/WatchCardRichHeader.js b/lib/parser/contents/WatchCardRichHeader.js index 486f6fbe5..e0c7e1ebf 100644 --- a/lib/parser/contents/WatchCardRichHeader.js +++ b/lib/parser/contents/WatchCardRichHeader.js @@ -6,8 +6,8 @@ class WatchCardRichHeader { type = 'WatchCardRichHeader'; constructor(item) { - this.title = new Text(item.title).text; - this.subtitle = new Text(item.subtitle).text; + this.title = new Text(item.title); + this.subtitle = new Text(item.subtitle); this.author = new Author(item, [item.titleBadge], item.avatar); this.author.name = this.title; } diff --git a/lib/parser/contents/index.js b/lib/parser/contents/index.js index f8378d3b6..b2ebf6676 100644 --- a/lib/parser/contents/index.js +++ b/lib/parser/contents/index.js @@ -51,6 +51,15 @@ class ResultsParser { gridChannelRenderer: require('./GridChannel'), reelShelfRenderer: require('./ReelShelf'), reelItemRenderer: require('./ReelItem'), + gridRenderer: require('./GenericList')('Grid', 'items'), + gridPlaylistRenderer: require('./GridPlaylist'), + backstagePostThreadRenderer: require('./BackstagePostThread'), + backstagePostRenderer: require('./BackstagePost'), + backstageImageRenderer: require('./BackstageImage'), + commentActionButtonsRenderer: require('./CommentActionButtons'), + buttonRenderer: require('./Button'), + toggleButtonRenderer: require('./ToggleButton'), + continuationItemRenderer: require('./ContinuationItem'), } const keys = Reflect.ownKeys(item); From e0e0f60985175e853cbc37816d753aff6fe76ce9 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sat, 30 Apr 2022 13:49:07 +0200 Subject: [PATCH 07/55] feat: channel full metadata --- .../contents/ChannelAboutFullMetadata.js | 22 +++++++++++++++++++ lib/parser/contents/NavigationEndpoint.js | 1 + lib/parser/contents/index.js | 1 + 3 files changed, 24 insertions(+) create mode 100644 lib/parser/contents/ChannelAboutFullMetadata.js diff --git a/lib/parser/contents/ChannelAboutFullMetadata.js b/lib/parser/contents/ChannelAboutFullMetadata.js new file mode 100644 index 000000000..395663dfb --- /dev/null +++ b/lib/parser/contents/ChannelAboutFullMetadata.js @@ -0,0 +1,22 @@ +const ResultsParser = require("."); +const Author = require("./Author"); +const NavigationEndpoint = require("./NavigationEndpoint"); +const Text = require("./Text"); + +class ChannelAboutFullMetadata { + type = 'ChannelAboutFullMetadata'; + + constructor(item) { + this.id = item.channelId; + this.canonical_channel_url = item.canonicalChannelUrl; + this.author = new Author(item.title, null, item.avatar); + this.views = new Text(item.viewCountText); + this.joined = new Text(item.joinedDateText); + this.description = new Text(item.description); + this.email_reveal = new NavigationEndpoint(item.onBusinessEmailRevealClickCommand); + this.can_reveal_email = !item.signInForBusinessEmail; + this.country = new Text(item.country); + } +} + +module.exports = ChannelAboutFullMetadata; \ No newline at end of file diff --git a/lib/parser/contents/NavigationEndpoint.js b/lib/parser/contents/NavigationEndpoint.js index 0874f99d0..ddf2d3a4d 100644 --- a/lib/parser/contents/NavigationEndpoint.js +++ b/lib/parser/contents/NavigationEndpoint.js @@ -44,6 +44,7 @@ class NavigationEndpoint { token: item.continuationCommand.token, trigger: item.trigger // TODO: is this the right place for this? } : null; + this.is_reveal_business_emal = !!item.revealBusinessEmailCommand; } } diff --git a/lib/parser/contents/index.js b/lib/parser/contents/index.js index b2ebf6676..8b4430ca1 100644 --- a/lib/parser/contents/index.js +++ b/lib/parser/contents/index.js @@ -60,6 +60,7 @@ class ResultsParser { buttonRenderer: require('./Button'), toggleButtonRenderer: require('./ToggleButton'), continuationItemRenderer: require('./ContinuationItem'), + channelAboutFullMetadataRenderer: require('./ChannelAboutFullMetadata'), } const keys = Reflect.ownKeys(item); From 4c93536379561ccba018e487e452d3d6a1be134d Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sat, 30 Apr 2022 14:16:30 +0200 Subject: [PATCH 08/55] feat: renderers for playlists --- lib/parser/contents/GridPlaylist.js | 1 + lib/parser/contents/NavigatableText.js | 1 + lib/parser/contents/NavigationEndpoint.js | 4 +++- lib/parser/contents/Playlist.js | 1 + lib/parser/contents/PlaylistVideo.js | 22 ++++++++++++++++++++++ lib/parser/contents/PlaylistVideoList.js | 14 ++++++++++++++ lib/parser/contents/Tab.js | 2 +- lib/parser/contents/index.js | 2 ++ 8 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 lib/parser/contents/PlaylistVideo.js create mode 100644 lib/parser/contents/PlaylistVideoList.js diff --git a/lib/parser/contents/GridPlaylist.js b/lib/parser/contents/GridPlaylist.js index e70dfe56c..12f6d38f7 100644 --- a/lib/parser/contents/GridPlaylist.js +++ b/lib/parser/contents/GridPlaylist.js @@ -13,6 +13,7 @@ class GridPlaylist { this.video_thumbnails = Array.isArray(item.sidebarThumbnails) ? item.sidebarThumbnails.map(thumbs => Thumbnail.fromResponse(thumbs)) : []; this.title = new Text(item.title); this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + this.view_playlist = new Text(item.viewPlaylistText); } } diff --git a/lib/parser/contents/NavigatableText.js b/lib/parser/contents/NavigatableText.js index f7c2b3c3b..f2bf03446 100644 --- a/lib/parser/contents/NavigatableText.js +++ b/lib/parser/contents/NavigatableText.js @@ -6,6 +6,7 @@ class NavigatableText extends Text { endpoint; constructor(node) { super(node); + // TODO: is this needed? Text now supports this itself this.endpoint = node.runs?.[0]?.navigationEndpoint ? new NavigationEndpoint(node.runs[0].navigationEndpoint) : diff --git a/lib/parser/contents/NavigationEndpoint.js b/lib/parser/contents/NavigationEndpoint.js index ddf2d3a4d..f7b25b6c8 100644 --- a/lib/parser/contents/NavigationEndpoint.js +++ b/lib/parser/contents/NavigationEndpoint.js @@ -19,7 +19,9 @@ class NavigationEndpoint { // this is the video id to navigate to this.watchVideoId = item.watchEndpoint ? { videoId: item.watchEndpoint.videoId, - playlistId: item.watchEndpoint.playlistId + playlistId: item.watchEndpoint.playlistId, + index: item.watchEndpoint.index, // this is the video index in the playlist + params: item.watchEndpoint.params, } : null; // this is a search button this.search = item.searchEndpoint ? { diff --git a/lib/parser/contents/Playlist.js b/lib/parser/contents/Playlist.js index 9f61fb81a..34e25daf7 100644 --- a/lib/parser/contents/Playlist.js +++ b/lib/parser/contents/Playlist.js @@ -16,6 +16,7 @@ class Playlist { this.videos = parseInt(item.videoCount); this.first_videos = ResultsParser.parse(item.videos); this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + this.view_playlist = new Text(item.viewPlaylistText); } } diff --git a/lib/parser/contents/PlaylistVideo.js b/lib/parser/contents/PlaylistVideo.js new file mode 100644 index 000000000..34997a726 --- /dev/null +++ b/lib/parser/contents/PlaylistVideo.js @@ -0,0 +1,22 @@ +const ResultsParser = require("."); +const NavigatableText = require("./NavigatableText"); +const NavigationEndpoint = require("./NavigationEndpoint"); +const Text = require("./Text"); +const Thumbnail = require("./Thumbnail"); + +class PlaylistVideo { + type = 'PlaylistVideo'; + + constructor(item) { + this.index = new Text(item.index).text; + this.is_playable = item.isPlayable; + this.duration = new Text(item.lengthText); + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + this.author = new NavigatableText(item.shortBylineText); + this.thumbnails = Thumbnail.fromResponse(item.thumbnail); + this.title = new Text(item.title); + this.id = item.videoId; + } +} + +module.exports = PlaylistVideo; \ No newline at end of file diff --git a/lib/parser/contents/PlaylistVideoList.js b/lib/parser/contents/PlaylistVideoList.js new file mode 100644 index 000000000..2be4587aa --- /dev/null +++ b/lib/parser/contents/PlaylistVideoList.js @@ -0,0 +1,14 @@ +const ResultsParser = require("."); + +class PlaylistVideoList { + type = 'PlaylistVideoList'; + + constructor(item) { + this.is_editable = item.isEditable; + this.can_reorder = item.canReorder; + this.id = item.playlistId; + this.contents = ResultsParser.parse(item.contents); + } +} + +module.exports = PlaylistVideoList; \ No newline at end of file diff --git a/lib/parser/contents/Tab.js b/lib/parser/contents/Tab.js index 255a28594..f5d1efe03 100644 --- a/lib/parser/contents/Tab.js +++ b/lib/parser/contents/Tab.js @@ -6,7 +6,7 @@ class Tab { constructor(item) { this.title = item.title; - this.endpoint = new NavigationEndpoint(item.endpoint); + this.endpoint = item.endpoint ? new NavigationEndpoint(item.endpoint) : null; this.selected = item.selected; // if this.selected then we may have content else we do not this.content = item.content ? ResultsParser.parseItem(item.content) : null; } diff --git a/lib/parser/contents/index.js b/lib/parser/contents/index.js index 8b4430ca1..35ca4143a 100644 --- a/lib/parser/contents/index.js +++ b/lib/parser/contents/index.js @@ -61,6 +61,8 @@ class ResultsParser { toggleButtonRenderer: require('./ToggleButton'), continuationItemRenderer: require('./ContinuationItem'), channelAboutFullMetadataRenderer: require('./ChannelAboutFullMetadata'), + playlistVideoListRenderer: require('./PlaylistVideoList'), + playlistVideoRenderer: require('./PlaylistVideo'), } const keys = Reflect.ownKeys(item); From e0105bc912af101f9d0dccc753bd2dcb247f1503 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sat, 30 Apr 2022 14:25:19 +0200 Subject: [PATCH 09/55] refactor!: Actions.browse Channels should be viewable when not logged in, also added 'navigation' type for use in NagivationEndpoint in the future. --- lib/core/Actions.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/core/Actions.js b/lib/core/Actions.js index 11b153cf7..9b5ada7f4 100644 --- a/lib/core/Actions.js +++ b/lib/core/Actions.js @@ -68,8 +68,8 @@ async function engage(session, engagement_type, args = {}) { * @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>} */ async function browse(session, action, args = {}) { - if (!session.logged_in && ![ 'home_feed', 'lyrics', - 'music_playlist', 'playlist', 'trending' ].includes(action)) + if (!session.logged_in && [ 'account_notifications', 'account_privacy', 'history', + 'library', 'subscriptions_feed' ].includes(action)) throw new Utils.InnertubeError('You are not signed in'); const data = { context: session.context }; @@ -96,6 +96,8 @@ async function browse(session, action, args = {}) { case 'subscriptions_feed': data.browseId = 'FEsubscriptions'; break; + case 'navigation': + args.params && (data.params = args.params); case 'channel': case 'playlist': data.browseId = args.browse_id; From 90f37a78abfe8820fba433e30886a45e65ee03ae Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sat, 30 Apr 2022 14:51:30 +0200 Subject: [PATCH 10/55] feat: home feed parsers --- lib/parser/contents/ChipCloudChip.js | 15 +++++++++++++++ lib/parser/contents/GenericContainer.js | 11 +++++++++++ lib/parser/contents/NavigationEndpoint.js | 2 +- lib/parser/contents/RichGrid.js | 14 ++++++++++++++ lib/parser/contents/RichShelf.js | 15 +++++++++++++++ lib/parser/contents/index.js | 6 ++++++ 6 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 lib/parser/contents/ChipCloudChip.js create mode 100644 lib/parser/contents/GenericContainer.js create mode 100644 lib/parser/contents/RichGrid.js create mode 100644 lib/parser/contents/RichShelf.js diff --git a/lib/parser/contents/ChipCloudChip.js b/lib/parser/contents/ChipCloudChip.js new file mode 100644 index 000000000..20e7ef56b --- /dev/null +++ b/lib/parser/contents/ChipCloudChip.js @@ -0,0 +1,15 @@ +const ResultsParser = require("."); +const NavigationEndpoint = require("./NavigationEndpoint"); +const Text = require("./Text"); + +class ChipCloudChip { + type = 'ChipCloudChip'; + + constructor(item) { + this.selected = item.selected; + this.endpoint = item.navigationEndpoint && new NavigationEndpoint(item.navigationEndpoint); + this.text = new Text(item.text); + } +} + +module.exports = ChipCloudChip; \ No newline at end of file diff --git a/lib/parser/contents/GenericContainer.js b/lib/parser/contents/GenericContainer.js new file mode 100644 index 000000000..31c05e20c --- /dev/null +++ b/lib/parser/contents/GenericContainer.js @@ -0,0 +1,11 @@ +const ResultsParser = require("."); + +module.exports = (name) => { + return class GenericContainer { + type = name; + + constructor(item) { + this.content = ResultsParser.parseItem(item.content); + } + } +}; \ No newline at end of file diff --git a/lib/parser/contents/NavigationEndpoint.js b/lib/parser/contents/NavigationEndpoint.js index f7b25b6c8..588978421 100644 --- a/lib/parser/contents/NavigationEndpoint.js +++ b/lib/parser/contents/NavigationEndpoint.js @@ -42,7 +42,7 @@ class NavigationEndpoint { } : null; // continuations this.continuation = item.continuationCommand ? { - request: item.continuationCommand.request, + request: item.continuationCommand.request, // 'CONTINUATION_REQUEST_TYPE_BROWSE' -> filter the browse results on home_feed? token: item.continuationCommand.token, trigger: item.trigger // TODO: is this the right place for this? } : null; diff --git a/lib/parser/contents/RichGrid.js b/lib/parser/contents/RichGrid.js new file mode 100644 index 000000000..180fb41e8 --- /dev/null +++ b/lib/parser/contents/RichGrid.js @@ -0,0 +1,14 @@ +const ResultsParser = require("."); + +class RichGrid { + type = 'RichGrid'; + + constructor(item) { + // XXX: we don't parse the masthead since it is usually an advertisement + // XXX: reflowOptions aren't parsed, I think its only used internally for layout + this.header = ResultsParser.parseItem(item.header); + this.contents = ResultsParser.parse(item.contents); + } +} + +module.exports = RichGrid; \ No newline at end of file diff --git a/lib/parser/contents/RichShelf.js b/lib/parser/contents/RichShelf.js new file mode 100644 index 000000000..c9ef9eee7 --- /dev/null +++ b/lib/parser/contents/RichShelf.js @@ -0,0 +1,15 @@ +const ResultsParser = require("."); +const NavigationEndpoint = require("./NavigationEndpoint"); +const Text = require("./Text"); + +class RichShelf { + type = 'RichShelf'; + + constructor(item) { + this.title = new Text(item.title); + this.contents = ResultsParser.parse(item.contents); + this.endpoint = item.endpoint ? new NavigationEndpoint(item.endpoint) : null; + } +} + +module.exports = RichShelf; \ No newline at end of file diff --git a/lib/parser/contents/index.js b/lib/parser/contents/index.js index 35ca4143a..130e2b5cb 100644 --- a/lib/parser/contents/index.js +++ b/lib/parser/contents/index.js @@ -63,6 +63,12 @@ class ResultsParser { channelAboutFullMetadataRenderer: require('./ChannelAboutFullMetadata'), playlistVideoListRenderer: require('./PlaylistVideoList'), playlistVideoRenderer: require('./PlaylistVideo'), + richGridRenderer: require('./RichGrid'), + feedFilterChipBarRenderer: require('./GenericList')('FeedFilterChipBar'), + chipCloudChipRenderer: require('./ChipCloudChip'), + richItemRenderer: require('./GenericContainer')('RichItem'), + richShelfRenderer: require('./RichShelf'), + richSectionRenderer: require('./GenericContainer')('RichSection'), } const keys = Reflect.ownKeys(item); From 0367cc61c0207b82a49d02f743a4ba7bc40b7d45 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sat, 30 Apr 2022 16:33:16 +0200 Subject: [PATCH 11/55] feat: watch page renderers --- lib/parser/contents/Button.js | 13 +++++++- lib/parser/contents/CompactVideo.js | 22 +++++++++++++ lib/parser/contents/Menu.js | 12 +++++++ lib/parser/contents/MenuNavigationItem.js | 14 ++++++++ lib/parser/contents/MenuServiceItem.js | 15 +++++++++ lib/parser/contents/MetadataRow.js | 13 ++++++++ lib/parser/contents/MetadataRowContainer.js | 11 +++++++ lib/parser/contents/MetadataRowHeader.js | 12 +++++++ lib/parser/contents/NavigationEndpoint.js | 11 +++++++ lib/parser/contents/PlaylistPanelVideo.js | 22 +++++++++++++ lib/parser/contents/Text.js | 12 +------ lib/parser/contents/TextRun.js | 14 ++++++++ lib/parser/contents/ToggleButton.js | 10 ++++++ .../contents/TwoColumnWatchNextResults.js | 32 +++++++++++++++++++ lib/parser/contents/VideoOwner.js | 16 ++++++++++ lib/parser/contents/VideoPrimaryInfo.js | 16 ++++++++++ lib/parser/contents/VideoSecondaryInfo.js | 15 +++++++++ lib/parser/contents/index.js | 12 +++++++ 18 files changed, 260 insertions(+), 12 deletions(-) create mode 100644 lib/parser/contents/CompactVideo.js create mode 100644 lib/parser/contents/Menu.js create mode 100644 lib/parser/contents/MenuNavigationItem.js create mode 100644 lib/parser/contents/MenuServiceItem.js create mode 100644 lib/parser/contents/MetadataRow.js create mode 100644 lib/parser/contents/MetadataRowContainer.js create mode 100644 lib/parser/contents/MetadataRowHeader.js create mode 100644 lib/parser/contents/PlaylistPanelVideo.js create mode 100644 lib/parser/contents/TextRun.js create mode 100644 lib/parser/contents/TwoColumnWatchNextResults.js create mode 100644 lib/parser/contents/VideoOwner.js create mode 100644 lib/parser/contents/VideoPrimaryInfo.js create mode 100644 lib/parser/contents/VideoSecondaryInfo.js diff --git a/lib/parser/contents/Button.js b/lib/parser/contents/Button.js index c811526b9..15fbacb67 100644 --- a/lib/parser/contents/Button.js +++ b/lib/parser/contents/Button.js @@ -6,10 +6,21 @@ class Button { type = 'Button'; constructor(item) { - this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + this.navigation_endpoint = item.navigationEndpoint && new NavigationEndpoint(item.navigationEndpoint); + this.service_endpoint = item.serviceEndpoint && new NavigationEndpoint(item.serviceEndpoint); this.text = new Text(item.text); this.tooltip = item.tooltip; } + + get endpoint() { + if (this.service_endpoint) { + return this.service_endpoint; + } + if (this.navigation_endpoint) { + return this.navigation_endpoint; + } + return null; + } } module.exports = Button; \ No newline at end of file diff --git a/lib/parser/contents/CompactVideo.js b/lib/parser/contents/CompactVideo.js new file mode 100644 index 000000000..73fa12081 --- /dev/null +++ b/lib/parser/contents/CompactVideo.js @@ -0,0 +1,22 @@ +const ResultsParser = require("."); +const Author = require("./Author"); +const NavigationEndpoint = require("./NavigationEndpoint"); +const Text = require("./Text"); +const Thumbnail = require("./Thumbnail"); + +class CompactVideo { + type = 'CompactVideo'; + + constructor(item) { + this.id = item.videoId; + this.thumbnails = Thumbnail.fromResponse(item.thumbnail); + this.title = new Text(item.title); + this.author = new Author(item.longBylineText, item.ownerBadges, item.channelThumbnail); + this.published_at = new Text(item.publishedTimeText); + this.views = new Text(item.viewCountText); + this.duration = new Text(item.lengthText); + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + } +} + +module.exports = CompactVideo; \ No newline at end of file diff --git a/lib/parser/contents/Menu.js b/lib/parser/contents/Menu.js new file mode 100644 index 000000000..ffabee81d --- /dev/null +++ b/lib/parser/contents/Menu.js @@ -0,0 +1,12 @@ +const ResultsParser = require("."); + +class Menu { + type = 'Menu'; + + constructor(item) { + this.top_level_buttons = item.topLevelButtons && ResultsParser.parse(item.topLevelButtons); + this.items = ResultsParser.parse(item.items); + } +} + +module.exports = Menu; \ No newline at end of file diff --git a/lib/parser/contents/MenuNavigationItem.js b/lib/parser/contents/MenuNavigationItem.js new file mode 100644 index 000000000..be3b013d2 --- /dev/null +++ b/lib/parser/contents/MenuNavigationItem.js @@ -0,0 +1,14 @@ +const ResultsParser = require("."); +const NavigationEndpoint = require("./NavigationEndpoint"); +const Text = require("./Text"); + +class MenuNavigationItem { + type = 'MenuNavigationItem'; + + constructor(item) { + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + this.text = new Text(item.text); + } +} + +module.exports = MenuNavigationItem; \ No newline at end of file diff --git a/lib/parser/contents/MenuServiceItem.js b/lib/parser/contents/MenuServiceItem.js new file mode 100644 index 000000000..9242bb46c --- /dev/null +++ b/lib/parser/contents/MenuServiceItem.js @@ -0,0 +1,15 @@ +const ResultsParser = require("."); +const NavigationEndpoint = require("./NavigationEndpoint"); +const Text = require("./Text"); + +class MenuServiceItem { + type = 'MenuServiceItem'; + + constructor(item) { + this.endpoint = new NavigationEndpoint(item.serviceEndpoint); + this.text = new Text(item.text); + // TODO: icons? + } +} + +module.exports = MenuServiceItem; \ No newline at end of file diff --git a/lib/parser/contents/MetadataRow.js b/lib/parser/contents/MetadataRow.js new file mode 100644 index 000000000..8e32187bf --- /dev/null +++ b/lib/parser/contents/MetadataRow.js @@ -0,0 +1,13 @@ +const ResultsParser = require("."); +const Text = require("./Text"); + +class MetadataRow { + type = 'MetadataRow'; + + constructor(item) { + this.contents = new Text(item.contents); + this.title = new Text(item.title); + } +} + +module.exports = MetadataRow; \ No newline at end of file diff --git a/lib/parser/contents/MetadataRowContainer.js b/lib/parser/contents/MetadataRowContainer.js new file mode 100644 index 000000000..14093321f --- /dev/null +++ b/lib/parser/contents/MetadataRowContainer.js @@ -0,0 +1,11 @@ +const ResultsParser = require("."); + +class MetadataRowContainer { + type = 'MetadataRowContainer'; + + constructor(item) { + this.rows = ResultsParser.parse(item.rows); + } +} + +module.exports = MetadataRowContainer; \ No newline at end of file diff --git a/lib/parser/contents/MetadataRowHeader.js b/lib/parser/contents/MetadataRowHeader.js new file mode 100644 index 000000000..afeeada73 --- /dev/null +++ b/lib/parser/contents/MetadataRowHeader.js @@ -0,0 +1,12 @@ +const ResultsParser = require("."); +const Text = require("./Text"); + +class MetadataRowHeader { + type = 'MetadataRowHeader'; + + constructor(item) { + this.text = new Text(item.content); + } +} + +module.exports = MetadataRowHeader; \ No newline at end of file diff --git a/lib/parser/contents/NavigationEndpoint.js b/lib/parser/contents/NavigationEndpoint.js index 588978421..e4e5395f7 100644 --- a/lib/parser/contents/NavigationEndpoint.js +++ b/lib/parser/contents/NavigationEndpoint.js @@ -1,3 +1,5 @@ +const ResultsParser = require("."); + class NavigationEndpoint { type = 'NavigationEndpoint'; url; @@ -47,6 +49,15 @@ class NavigationEndpoint { trigger: item.trigger // TODO: is this the right place for this? } : null; this.is_reveal_business_emal = !!item.revealBusinessEmailCommand; + // TODO: sign in endpoints + + // TODO: modal endpoints cleanup + const modalRenderer = item.modalEndpoint?.moadlWithTitleAndButtonRenderer; + this.modal = modalRenderer ? { + title: modalRenderer.title, + button: ResultsParser.parseItem(modalRenderer.button), + content: modalRenderer.content, + } : null; } } diff --git a/lib/parser/contents/PlaylistPanelVideo.js b/lib/parser/contents/PlaylistPanelVideo.js new file mode 100644 index 000000000..fada0a0bb --- /dev/null +++ b/lib/parser/contents/PlaylistPanelVideo.js @@ -0,0 +1,22 @@ +const ResultsParser = require("."); +const Author = require("./Author"); +const NavigationEndpoint = require("./NavigationEndpoint"); +const Text = require("./Text"); +const Thumbnail = require("./Thumbnail"); + +class PlaylistPanelVideo { + type = 'PlaylistPanelVideo'; + + constructor(item) { + this.index = new Text(item.indexText).text; + this.selected = item.selected; + this.duration = new Text(item.lengthText); + this.author = new Author(item.longBylineText); + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + this.thumbnails = Thumbnail.fromResponse(item.thumbnail); + this.title = new Text(item.title); + this.id = item.videoId; + } +} + +module.exports = PlaylistPanelVideo; \ No newline at end of file diff --git a/lib/parser/contents/Text.js b/lib/parser/contents/Text.js index 46e05428d..ffb1ea45f 100644 --- a/lib/parser/contents/Text.js +++ b/lib/parser/contents/Text.js @@ -1,4 +1,4 @@ -const NavigationEndpoint = require("./NavigationEndpoint"); +const TextRun = require("./TextRun"); class Text { type = 'Text'; @@ -32,14 +32,4 @@ class Text { } } -class TextRun { - type = 'TextRun'; - text; - endpoint; - constructor(node) { - this.text = node.text; - this.endpoint = node.navigationEndpoint ? new NavigationEndpoint(node.navigationEndpoint) : null; - } -} - module.exports = Text; \ No newline at end of file diff --git a/lib/parser/contents/TextRun.js b/lib/parser/contents/TextRun.js new file mode 100644 index 000000000..76fee75bd --- /dev/null +++ b/lib/parser/contents/TextRun.js @@ -0,0 +1,14 @@ +const ResultsParser = require("."); +const NavigationEndpoint = require("./NavigationEndpoint"); + +class TextRun { + type = 'TextRun'; + text; + endpoint; + constructor(node) { + this.text = node.text; + this.endpoint = node.navigationEndpoint && new NavigationEndpoint(node.navigationEndpoint); + } +} + +module.exports = TextRun; \ No newline at end of file diff --git a/lib/parser/contents/ToggleButton.js b/lib/parser/contents/ToggleButton.js index 3124972f6..d90d8f75e 100644 --- a/lib/parser/contents/ToggleButton.js +++ b/lib/parser/contents/ToggleButton.js @@ -1,5 +1,6 @@ const ResultsParser = require("."); const NavigationEndpoint = require("./NavigationEndpoint"); +const Text = require("./Text"); class ToggleButton { type = 'ToggleButton'; @@ -12,6 +13,8 @@ class ToggleButton { this.default_navigation_endpoint = item.defaultNavigationEndpoint && new NavigationEndpoint(item.defaultNavigationEndpoint); this.default_tooltip = item.defaultTooltip; this.toggled_tooltip = item.toggledTooltip; + this.default_text = new Text(item.defaultText); + this.toggled_text = new Text(item.toggledText); } get endpoint() { @@ -33,6 +36,13 @@ class ToggleButton { } return this.default_tooltip; } + + get text() { + if (this.is_toggled) { + return this.toggled_text; + } + return this.default_text; + } } module.exports = ToggleButton; \ No newline at end of file diff --git a/lib/parser/contents/TwoColumnWatchNextResults.js b/lib/parser/contents/TwoColumnWatchNextResults.js new file mode 100644 index 000000000..b024538c5 --- /dev/null +++ b/lib/parser/contents/TwoColumnWatchNextResults.js @@ -0,0 +1,32 @@ +const ResultsParser = require("."); +const Author = require("./Author"); +const NavigatableText = require("./NavigatableText"); +const NavigationEndpoint = require("./NavigationEndpoint"); + +class TwoColumnWatchNextResult { + type = 'TwoColumnWatchNextResult'; + + constructor(item) { + // XXX: do we need this? + // this.autoplay + // this contains the video info + this.primary = ResultsParser.parse(item.results.results.contents); + // these hold the recommendations + this.secondary = ResultsParser.parse(item.secondaryResults.secondaryResults.results); + // playlist data + const playlist = item.playlist?.playlist; + this.playlist = playlist && { + current_index: playlist.currentIndex, + endpoint: new NavigationEndpoint(playlist.endpoint), + is_course: playlist.isCourse, + is_infinite: playlist.isInfinite, + author: new Author(playlist.longBylineText, playlist.ownerBadges), + save: playlist.saveButton && ResultsParser.parseItem(playlist.saveButton), + title: new NavigatableText(playlist.titleText), + videos: playlist.totalVideos, + contents: ResultsParser.parse(playlist.contents), + } + } +} + +module.exports = TwoColumnWatchNextResult; \ No newline at end of file diff --git a/lib/parser/contents/VideoOwner.js b/lib/parser/contents/VideoOwner.js new file mode 100644 index 000000000..0bb7b0be8 --- /dev/null +++ b/lib/parser/contents/VideoOwner.js @@ -0,0 +1,16 @@ +const ResultsParser = require("."); +const Author = require("./Author"); + +class VideoOwner { + type = 'VideoOwner'; + + constructor(item) { + /* + this.author = new Author({ + ...item.title, + navigationEndpoint: item.navigationEndpoint + }, item.badges, item.thumbnail); */ + } +} + +module.exports = VideoOwner; \ No newline at end of file diff --git a/lib/parser/contents/VideoPrimaryInfo.js b/lib/parser/contents/VideoPrimaryInfo.js new file mode 100644 index 000000000..a73d70ef4 --- /dev/null +++ b/lib/parser/contents/VideoPrimaryInfo.js @@ -0,0 +1,16 @@ +const ResultsParser = require("."); +const Text = require("./Text"); + +class VideoPrimaryInfo { + type = 'VideoPrimaryInfo'; + + constructor(item) { + this.title = new Text(item.title); + this.published_at = new Date(item.dateText); + // menuRenderer + this.actions = ResultsParser.parseItem(item.videoActions); + this.views = new Text(item.viewCount.videoViewCountRenderer?.viewCount || item.viewCount.videoViewCountRenderer?.shortViewCount); + } +} + +module.exports = VideoPrimaryInfo; \ No newline at end of file diff --git a/lib/parser/contents/VideoSecondaryInfo.js b/lib/parser/contents/VideoSecondaryInfo.js new file mode 100644 index 000000000..79556d6e1 --- /dev/null +++ b/lib/parser/contents/VideoSecondaryInfo.js @@ -0,0 +1,15 @@ +const ResultsParser = require("."); +const Text = require("./Text"); + +class VideoSecondaryInfo { + type = 'VideoSecondaryInfo'; + + constructor(item) { + this.owner = ResultsParser.parseItem(item.owner); + this.description = new Text(item.description); + this.metadata = ResultsParser.parseItem(item.metadataRowContainer); + this.description_collapsed_lines = item.descriptionCollapsedLines; + } +} + +module.exports = VideoSecondaryInfo; \ No newline at end of file diff --git a/lib/parser/contents/index.js b/lib/parser/contents/index.js index 130e2b5cb..4823268fb 100644 --- a/lib/parser/contents/index.js +++ b/lib/parser/contents/index.js @@ -69,6 +69,18 @@ class ResultsParser { richItemRenderer: require('./GenericContainer')('RichItem'), richShelfRenderer: require('./RichShelf'), richSectionRenderer: require('./GenericContainer')('RichSection'), + twoColumnWatchNextResults: require('./TwoColumnWatchNextResults'), + videoPrimaryInfoRenderer: require('./VideoPrimaryInfo'), + menuRenderer: require('./Menu'), + menuNavigationItemRenderer: require('./MenuNavigationItem'), + menuServiceItemRenderer: require('./MenuServiceItem'), + videoSecondaryInfoRenderer: require('./VideoSecondaryInfo'), + videoOwnerRenderer: require('./VideoOwner'), + metadataRowContainerRenderer: require('./MetadataRowContainer'), + metadataRowHeaderRenderer: require('./MetadataRowHeader'), + metadataRowRenderer: require('./MetadataRow'), + compactVideoRenderer: require('./CompactVideo'), + playlistPanelVideoRenderer: require('./PlaylistPanelVideo'), } const keys = Reflect.ownKeys(item); From a76bab425ca72e353f55c5aada2eff046755c8eb Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sun, 1 May 2022 01:57:40 +0200 Subject: [PATCH 12/55] feat: start implementing HomeFeed API The HomeFeed class remains compatible with the existing API --- lib/Innertube.js | 9 +- lib/core/HomeFeed.js | 91 +++++++++++++++++++ lib/parser/contents/Author.js | 2 +- lib/parser/contents/CompactVideo.js | 1 + lib/parser/contents/GridVideo.js | 1 + lib/parser/contents/MovingThumbnail.js | 12 +++ .../contents/TwoColumnWatchNextResults.js | 2 + lib/parser/contents/Video.js | 12 ++- lib/parser/contents/index.js | 15 +-- lib/parser/simplify.js | 81 +++++++++++++++++ 10 files changed, 206 insertions(+), 20 deletions(-) create mode 100644 lib/core/HomeFeed.js create mode 100644 lib/parser/contents/MovingThumbnail.js create mode 100644 lib/parser/simplify.js diff --git a/lib/Innertube.js b/lib/Innertube.js index 706a7c0cd..109400a6b 100644 --- a/lib/Innertube.js +++ b/lib/Innertube.js @@ -18,6 +18,8 @@ const Constants = require('./utils/Constants'); const Proto = require('./proto'); const NToken = require('./deciphers/NToken'); const Signature = require('./deciphers/Signature'); +const ResultsParser = require('./parser/contents'); +const HomeFeed = require('./core/HomeFeed'); class Innertube { #oauth; @@ -548,13 +550,8 @@ class Innertube { async getHomeFeed() { const response = await Actions.browse(this, 'home_feed'); if (!response.success) throw new Utils.InnertubeError('Could not retrieve home feed', response); - - const homefeed = new Parser(this, response, { - client: 'YOUTUBE', - data_type: 'HOMEFEED' - }).parse(); - return homefeed; + return new HomeFeed(response.data); } /** diff --git a/lib/core/HomeFeed.js b/lib/core/HomeFeed.js new file mode 100644 index 000000000..8d95234f9 --- /dev/null +++ b/lib/core/HomeFeed.js @@ -0,0 +1,91 @@ +const ResultsParser = require("../parser/contents"); +const Simplify = require("../parser/simplify"); +const Utils = require("../utils/Utils"); + +class HomeFeed { + #page; + #videos; + #video_elements; + constructor(data) { + this.#page = ResultsParser.parseResponse(data); + } + + /** + * Get the original page data + */ + get page() { + return this.#page; + } + + /** + * Get all the videos in the home feed + * @returns {Array} + */ + getVideos() { + if (this.#video_elements) return this.#video_elements; + + this.#video_elements = Simplify.matching({ + type: Simplify.matching(/^Video$/), + }).runOn(this.#page); + + return this.#video_elements; + } + + /** + * Get all the videos in the home feed + * @deprecated Use getVideos instead + */ + get videos() { + if (this.#videos) return this.#videos; + + const simplified = this.getVideos(); + + this.#videos = simplified.map(video => { + return { + id: video.id, + title: video.title.toString(), + description: video.description, + channel: { + id: video.author.id, + name: video.author.name, + url: video.author.endpoint.metadata.url, + thumbnail: video.author.best_thumbnail, + is_verified: video.author.is_verified, + is_verified_artist: video.author.is_verified_artist, + }, + metadata: { + view_count: video.views.toString(), + short_view_count_text: { + simple_text: video.short_view_count.toString(), + // TODO: the accessiblity text is not yet parsed + accessibility_label: "", + }, + thumbnail: video.best_thumbnail, + thumbnails: video.thumbnails, + // XXX: It doesn't look like this is returned? + // but I'll send it anyway + moving_thumbnail: video.rich_thumbnail?.thumbnails?.[0] || {}, + moving_thumbnails: video.rich_thumbnail?.thumbnails || [], + published: video.published_at.toString(), + duration: { + seconds: Utils.timeToSeconds(video.duration.toString()), + simple_text: video.duration.toString(), + // TODO: again - we need access to the accessibility data here + accessibility_label: "", + }, + // XXX: this is different return types from the existing API + badges: video.badges, + owner_badges: video.author.badges, + } + } + }); + + return this.#videos; + } + + getContinuation() { + return; // TODO: implement this! + } +} + +module.exports = HomeFeed; diff --git a/lib/parser/contents/Author.js b/lib/parser/contents/Author.js index d3f9db550..3e0fc177d 100644 --- a/lib/parser/contents/Author.js +++ b/lib/parser/contents/Author.js @@ -17,7 +17,7 @@ class Author { } get name() { - return this.#nav_text.text; + return this.#nav_text.toString(); } set name(name) { diff --git a/lib/parser/contents/CompactVideo.js b/lib/parser/contents/CompactVideo.js index 73fa12081..c5eebd789 100644 --- a/lib/parser/contents/CompactVideo.js +++ b/lib/parser/contents/CompactVideo.js @@ -10,6 +10,7 @@ class CompactVideo { constructor(item) { this.id = item.videoId; this.thumbnails = Thumbnail.fromResponse(item.thumbnail); + this.rich_thumbnail = item.richThumbnail && ResultsParser.parseItem(item.richThumbnail); this.title = new Text(item.title); this.author = new Author(item.longBylineText, item.ownerBadges, item.channelThumbnail); this.published_at = new Text(item.publishedTimeText); diff --git a/lib/parser/contents/GridVideo.js b/lib/parser/contents/GridVideo.js index ce731b3a3..41cbca658 100644 --- a/lib/parser/contents/GridVideo.js +++ b/lib/parser/contents/GridVideo.js @@ -9,6 +9,7 @@ class GridVideo { constructor(item) { this.id = item.videoId; this.thumbnails = Thumbnail.fromResponse(item.thumbnail); + this.rich_thumbnail = item.richThumbnail && ResultsParser.parseItem(item.richThumbnail); this.title = new Text(item.title, ''); this.badges = Array.isArray(item.badges) ? ResultsParser.parse(item.badges) : []; const lengthAlt = item.thumbnailOverlays.find(overlay => overlay.hasOwnProperty('thumbnailOverlayTimeStatusRenderer'))?.thumbnailOverlayTimeStatusRenderer; diff --git a/lib/parser/contents/MovingThumbnail.js b/lib/parser/contents/MovingThumbnail.js new file mode 100644 index 000000000..2a3dc26e5 --- /dev/null +++ b/lib/parser/contents/MovingThumbnail.js @@ -0,0 +1,12 @@ +const ResultsParser = require("."); +const Thumbnail = require("./Thumbnail"); + +class MovingThumbnail { + type = 'MovingThumbnail'; + + constructor(item) { + this.thumbnails = item.movingThumbnailDetails && Thumbnail.fromResponse(item.movingThumbnailDetails.thumbnails); + } +} + +module.exports = MovingThumbnail; \ No newline at end of file diff --git a/lib/parser/contents/TwoColumnWatchNextResults.js b/lib/parser/contents/TwoColumnWatchNextResults.js index b024538c5..5cb550ae1 100644 --- a/lib/parser/contents/TwoColumnWatchNextResults.js +++ b/lib/parser/contents/TwoColumnWatchNextResults.js @@ -26,6 +26,8 @@ class TwoColumnWatchNextResult { videos: playlist.totalVideos, contents: ResultsParser.parse(playlist.contents), } + // TODO: conversationBar + // this.conversation = liveChatRenderer } } diff --git a/lib/parser/contents/Video.js b/lib/parser/contents/Video.js index 9cbb2cef3..3c858708c 100644 --- a/lib/parser/contents/Video.js +++ b/lib/parser/contents/Video.js @@ -12,12 +12,13 @@ class Video { this.badges = Array.isArray(item.badges) ? ResultsParser.parse(item.badges) : []; // TODO: verify this works this.thumbnails = Thumbnail.fromResponse(item.thumbnail); + this.rich_thumbnail = item.richThumbnail && ResultsParser.parseItem(item.richThumbnail); const upcoming = item.upcomingEventData ? Number(`${item.upcomingEventData.startTime}000`) : null; if (upcoming) this.upcoming = new Date(upcoming); this.id = item.videoId; this.title = new Text(item.title, ''); const lengthAlt = item.thumbnailOverlays.find(overlay => overlay.hasOwnProperty('thumbnailOverlayTimeStatusRenderer'))?.thumbnailOverlayTimeStatusRenderer; - this.duration = item.lengthText ? new Text(item.lengthText, '') : lengthAlt?.text ? new Text(lengthAlt.text) : ''; + this.duration = item.lengthText ? new Text(item.lengthText, '') : lengthAlt?.text ? new Text(lengthAlt.text).toString() : ''; this.published_at = new Text(item.publishedTimeText, ''); this.views = new Text(item.viewCountText, ''); // TODO: might be simplified? this appears to only contain the description @@ -25,9 +26,18 @@ class Video { text: new Text(snip.snippetText, ''), hoverText: new Text(snip.snippetHoverText, ''), })); + this.description_snippet = item.descriptionSnippet ? new Text(item.descriptionSnippet, '') : null; + this.short_view_count = new Text(item.shortViewCountText, ''); this.endpoint = new NavigationEndpoint(item.navigationEndpoint); } + get description() { + if (this.snippets) { + return this.snippets.map(snip => snip.text.toString()).join('') + } + return this.description_snippet?.toString() || ''; + } + get is_live() { return this.badges.some(badge => badge.style === 'BADGE_STYLE_TYPE_LIVE_NOW'); } diff --git a/lib/parser/contents/index.js b/lib/parser/contents/index.js index 4823268fb..d1c05009a 100644 --- a/lib/parser/contents/index.js +++ b/lib/parser/contents/index.js @@ -1,17 +1,7 @@ class ResultsParser { - static parseSearch(results) { + static parseResponse(data) { return { - contents: ResultsParser.parse(results.contents), - refinements: results.refinements || [], - estimated_results: parseInt(results.estimated_results), - } - } - - static parseChannel(results) { - return { - header: ResultsParser.parseItem(results.header), - metadata: ResultsParser.parseItem(results.metadata), - contents: ResultsParser.parse(results.contents), + contents: ResultsParser.parseItem(data.contents) } } @@ -81,6 +71,7 @@ class ResultsParser { metadataRowRenderer: require('./MetadataRow'), compactVideoRenderer: require('./CompactVideo'), playlistPanelVideoRenderer: require('./PlaylistPanelVideo'), + movingThumbnailRenderer: require('./MovingThumbnail'), } const keys = Reflect.ownKeys(item); diff --git a/lib/parser/simplify.js b/lib/parser/simplify.js new file mode 100644 index 000000000..105fe5b55 --- /dev/null +++ b/lib/parser/simplify.js @@ -0,0 +1,81 @@ +class Simplify { + #schema; + constructor(schema) { + this.#schema = schema; + } + + static matching(match) { + if (match instanceof RegExp) { + return new Simplify({ + type: 'regexmatch', + match, + }); + } + if ([ 'string', 'number', 'boolean' ].includes(typeof match)) { + return new Simplify({ + type: 'equalitymatch', + match, + }); + } + return new Simplify({ + type: 'match', + allOf: match, + }); + } + + static matchingAny(match) { + if (!Array.isArray(match)) { + throw new Error('matchingAny expects an array'); + } + return new Simplify({ + type: 'matchany', + match: match.map(m => Simplify.matching(m)), + }); + } + + match(data) { + const { allOf } = this.#schema; + let currentObject = data; + const queuedObjects = []; + const results = []; + const keysToMatch = Object.keys(allOf); + while(currentObject) { + const targetKeys = Object.keys(currentObject); + const matches = keysToMatch.every(key => { + return targetKeys.includes(key) && allOf[key].runOn(currentObject[key]); + }) + if (matches) { + results.push(currentObject); + } + for (const key in currentObject) { + const value = currentObject[key]; + if (value && typeof value === 'object') { + queuedObjects.push(value); + } + } + currentObject = queuedObjects.shift(); + } + return results; + } + + runOn(data) { + switch (this.#schema.type) { + case 'match': + return this.match(data); + + case 'regexmatch': + return typeof data === 'string' && data.match(this.#schema.match); + + case 'equalitymatch': + return data === this.#schema.match; + + case 'matchany': + return this.#schema.match.some(m => m.runOn(data)); + + default: + break; + } + } +} + +module.exports = Simplify; \ No newline at end of file From bdadb1bc35e85701803f4396f6890eddaaf7ca8c Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sun, 1 May 2022 02:47:08 +0200 Subject: [PATCH 13/55] feat: generate types using tsc and jsdoc --- lib/parser/contents/Author.js | 20 +- lib/parser/contents/NavigationEndpoint.js | 20 +- lib/parser/contents/Thumbnail.js | 17 + lib/parser/contents/Video.js | 79 ++- package.json | 8 +- tsconfig.json | 16 + typings/index.d.ts | 192 +------ typings/lib/Innertube.d.ts | 511 ++++++++++++++++++ typings/lib/core/Actions.d.ts | 157 ++++++ typings/lib/core/HomeFeed.d.ts | 22 + typings/lib/core/Livechat.d.ts | 48 ++ typings/lib/core/OAuth.d.ts | 35 ++ typings/lib/core/Player.d.ts | 11 + typings/lib/deciphers/NToken.d.ts | 12 + typings/lib/deciphers/Signature.d.ts | 13 + typings/lib/parser/contents/Author.d.ts | 30 + .../lib/parser/contents/BackstageImage.d.ts | 7 + .../lib/parser/contents/BackstagePost.d.ts | 15 + .../parser/contents/BackstagePostThread.d.ts | 6 + typings/lib/parser/contents/Boilerplate.d.ts | 5 + typings/lib/parser/contents/Button.d.ts | 12 + typings/lib/parser/contents/Channel.d.ts | 14 + .../contents/ChannelAboutFullMetadata.d.ts | 17 + .../lib/parser/contents/ChannelMetadata.d.ts | 21 + .../parser/contents/ChannelVideoPlayer.d.ts | 11 + typings/lib/parser/contents/ChildVideo.d.ts | 11 + .../lib/parser/contents/ChipCloudChip.d.ts | 10 + .../lib/parser/contents/CollageHeroImage.d.ts | 11 + .../parser/contents/CommentActionButtons.d.ts | 8 + typings/lib/parser/contents/CompactVideo.d.ts | 18 + .../lib/parser/contents/ContinuationItem.d.ts | 7 + .../lib/parser/contents/ExpandableTab.d.ts | 10 + .../lib/parser/contents/GenericContainer.d.ts | 7 + typings/lib/parser/contents/GenericList.d.ts | 8 + typings/lib/parser/contents/GridChannel.d.ts | 14 + typings/lib/parser/contents/GridPlaylist.d.ts | 15 + typings/lib/parser/contents/GridVideo.d.ts | 17 + .../parser/contents/HorizontalCardList.d.ts | 8 + .../lib/parser/contents/HorizontalList.d.ts | 7 + typings/lib/parser/contents/Menu.d.ts | 7 + .../parser/contents/MenuNavigationItem.d.ts | 9 + .../lib/parser/contents/MenuServiceItem.d.ts | 9 + .../lib/parser/contents/MetadataBadge.d.ts | 8 + typings/lib/parser/contents/MetadataRow.d.ts | 8 + .../parser/contents/MetadataRowContainer.d.ts | 6 + .../parser/contents/MetadataRowHeader.d.ts | 7 + typings/lib/parser/contents/Mix.d.ts | 6 + .../lib/parser/contents/MovingThumbnail.d.ts | 7 + .../lib/parser/contents/NavigatableText.d.ts | 9 + .../parser/contents/NavigationEndpoint.d.ts | 50 ++ typings/lib/parser/contents/Playlist.d.ts | 16 + .../parser/contents/PlaylistPanelVideo.d.ts | 17 + .../lib/parser/contents/PlaylistVideo.d.ts | 17 + .../parser/contents/PlaylistVideoList.d.ts | 9 + typings/lib/parser/contents/ReelItem.d.ts | 13 + typings/lib/parser/contents/ReelShelf.d.ts | 10 + typings/lib/parser/contents/RichGrid.d.ts | 7 + typings/lib/parser/contents/RichShelf.d.ts | 10 + .../parser/contents/SearchRefinementCard.d.ts | 12 + typings/lib/parser/contents/Shelf.d.ts | 15 + typings/lib/parser/contents/Tab.d.ts | 10 + typings/lib/parser/contents/Text.d.ts | 12 + typings/lib/parser/contents/TextRun.d.ts | 8 + typings/lib/parser/contents/Thumbnail.d.ts | 26 + typings/lib/parser/contents/ToggleButton.d.ts | 19 + .../contents/TwoColumnBrowseResults.d.ts | 6 + .../contents/TwoColumnSearchResults.d.ts | 7 + .../contents/TwoColumnWatchNextResults.d.ts | 21 + .../parser/contents/UniversalWatchCard.d.ts | 8 + typings/lib/parser/contents/VerticalList.d.ts | 7 + typings/lib/parser/contents/Video.d.ts | 88 +++ typings/lib/parser/contents/VideoOwner.d.ts | 5 + .../lib/parser/contents/VideoPrimaryInfo.d.ts | 10 + .../parser/contents/VideoSecondaryInfo.d.ts | 10 + .../contents/WatchCardCompactVideo.d.ts | 14 + .../parser/contents/WatchCardHeroVideo.d.ts | 8 + .../parser/contents/WatchCardRichHeader.d.ts | 10 + .../contents/WatchCardSectionSequence.d.ts | 6 + typings/lib/parser/contents/index.d.ts | 8 + typings/lib/parser/index.d.ts | 9 + typings/lib/parser/simplify.d.ts | 9 + typings/lib/parser/youtube/index.d.ts | 11 + .../youtube/others/ChannelMetadata.d.ts | 15 + .../parser/youtube/others/CommentThread.d.ts | 23 + .../youtube/others/GridPlaylistItem.d.ts | 12 + .../parser/youtube/others/GridVideoItem.d.ts | 25 + .../youtube/others/NotificationItem.d.ts | 14 + .../parser/youtube/others/PlaylistItem.d.ts | 15 + .../parser/youtube/others/ShelfRenderer.d.ts | 9 + .../lib/parser/youtube/others/VideoItem.d.ts | 31 ++ .../youtube/search/SearchSuggestionItem.d.ts | 4 + .../youtube/search/VideoResultItem.d.ts | 31 ++ typings/lib/parser/ytmusic/index.d.ts | 9 + .../parser/ytmusic/others/PlaylistItem.d.ts | 14 + .../ytmusic/search/AlbumResultItem.d.ts | 11 + .../ytmusic/search/ArtistResultItem.d.ts | 10 + .../search/MusicSearchSuggestionItem.d.ts | 8 + .../ytmusic/search/PlaylistResultItem.d.ts | 11 + .../parser/ytmusic/search/SongResultItem.d.ts | 12 + .../parser/ytmusic/search/TopResultItem.d.ts | 4 + .../ytmusic/search/VideoResultItem.d.ts | 12 + typings/lib/proto/index.d.ts | 11 + typings/lib/proto/messages.d.ts | 66 +++ typings/lib/utils/Constants.d.ts | 101 ++++ typings/lib/utils/Request.d.ts | 7 + typings/lib/utils/Utils.d.ts | 70 +++ 106 files changed, 2331 insertions(+), 208 deletions(-) create mode 100644 tsconfig.json create mode 100644 typings/lib/Innertube.d.ts create mode 100644 typings/lib/core/Actions.d.ts create mode 100644 typings/lib/core/HomeFeed.d.ts create mode 100644 typings/lib/core/Livechat.d.ts create mode 100644 typings/lib/core/OAuth.d.ts create mode 100644 typings/lib/core/Player.d.ts create mode 100644 typings/lib/deciphers/NToken.d.ts create mode 100644 typings/lib/deciphers/Signature.d.ts create mode 100644 typings/lib/parser/contents/Author.d.ts create mode 100644 typings/lib/parser/contents/BackstageImage.d.ts create mode 100644 typings/lib/parser/contents/BackstagePost.d.ts create mode 100644 typings/lib/parser/contents/BackstagePostThread.d.ts create mode 100644 typings/lib/parser/contents/Boilerplate.d.ts create mode 100644 typings/lib/parser/contents/Button.d.ts create mode 100644 typings/lib/parser/contents/Channel.d.ts create mode 100644 typings/lib/parser/contents/ChannelAboutFullMetadata.d.ts create mode 100644 typings/lib/parser/contents/ChannelMetadata.d.ts create mode 100644 typings/lib/parser/contents/ChannelVideoPlayer.d.ts create mode 100644 typings/lib/parser/contents/ChildVideo.d.ts create mode 100644 typings/lib/parser/contents/ChipCloudChip.d.ts create mode 100644 typings/lib/parser/contents/CollageHeroImage.d.ts create mode 100644 typings/lib/parser/contents/CommentActionButtons.d.ts create mode 100644 typings/lib/parser/contents/CompactVideo.d.ts create mode 100644 typings/lib/parser/contents/ContinuationItem.d.ts create mode 100644 typings/lib/parser/contents/ExpandableTab.d.ts create mode 100644 typings/lib/parser/contents/GenericContainer.d.ts create mode 100644 typings/lib/parser/contents/GenericList.d.ts create mode 100644 typings/lib/parser/contents/GridChannel.d.ts create mode 100644 typings/lib/parser/contents/GridPlaylist.d.ts create mode 100644 typings/lib/parser/contents/GridVideo.d.ts create mode 100644 typings/lib/parser/contents/HorizontalCardList.d.ts create mode 100644 typings/lib/parser/contents/HorizontalList.d.ts create mode 100644 typings/lib/parser/contents/Menu.d.ts create mode 100644 typings/lib/parser/contents/MenuNavigationItem.d.ts create mode 100644 typings/lib/parser/contents/MenuServiceItem.d.ts create mode 100644 typings/lib/parser/contents/MetadataBadge.d.ts create mode 100644 typings/lib/parser/contents/MetadataRow.d.ts create mode 100644 typings/lib/parser/contents/MetadataRowContainer.d.ts create mode 100644 typings/lib/parser/contents/MetadataRowHeader.d.ts create mode 100644 typings/lib/parser/contents/Mix.d.ts create mode 100644 typings/lib/parser/contents/MovingThumbnail.d.ts create mode 100644 typings/lib/parser/contents/NavigatableText.d.ts create mode 100644 typings/lib/parser/contents/NavigationEndpoint.d.ts create mode 100644 typings/lib/parser/contents/Playlist.d.ts create mode 100644 typings/lib/parser/contents/PlaylistPanelVideo.d.ts create mode 100644 typings/lib/parser/contents/PlaylistVideo.d.ts create mode 100644 typings/lib/parser/contents/PlaylistVideoList.d.ts create mode 100644 typings/lib/parser/contents/ReelItem.d.ts create mode 100644 typings/lib/parser/contents/ReelShelf.d.ts create mode 100644 typings/lib/parser/contents/RichGrid.d.ts create mode 100644 typings/lib/parser/contents/RichShelf.d.ts create mode 100644 typings/lib/parser/contents/SearchRefinementCard.d.ts create mode 100644 typings/lib/parser/contents/Shelf.d.ts create mode 100644 typings/lib/parser/contents/Tab.d.ts create mode 100644 typings/lib/parser/contents/Text.d.ts create mode 100644 typings/lib/parser/contents/TextRun.d.ts create mode 100644 typings/lib/parser/contents/Thumbnail.d.ts create mode 100644 typings/lib/parser/contents/ToggleButton.d.ts create mode 100644 typings/lib/parser/contents/TwoColumnBrowseResults.d.ts create mode 100644 typings/lib/parser/contents/TwoColumnSearchResults.d.ts create mode 100644 typings/lib/parser/contents/TwoColumnWatchNextResults.d.ts create mode 100644 typings/lib/parser/contents/UniversalWatchCard.d.ts create mode 100644 typings/lib/parser/contents/VerticalList.d.ts create mode 100644 typings/lib/parser/contents/Video.d.ts create mode 100644 typings/lib/parser/contents/VideoOwner.d.ts create mode 100644 typings/lib/parser/contents/VideoPrimaryInfo.d.ts create mode 100644 typings/lib/parser/contents/VideoSecondaryInfo.d.ts create mode 100644 typings/lib/parser/contents/WatchCardCompactVideo.d.ts create mode 100644 typings/lib/parser/contents/WatchCardHeroVideo.d.ts create mode 100644 typings/lib/parser/contents/WatchCardRichHeader.d.ts create mode 100644 typings/lib/parser/contents/WatchCardSectionSequence.d.ts create mode 100644 typings/lib/parser/contents/index.d.ts create mode 100644 typings/lib/parser/index.d.ts create mode 100644 typings/lib/parser/simplify.d.ts create mode 100644 typings/lib/parser/youtube/index.d.ts create mode 100644 typings/lib/parser/youtube/others/ChannelMetadata.d.ts create mode 100644 typings/lib/parser/youtube/others/CommentThread.d.ts create mode 100644 typings/lib/parser/youtube/others/GridPlaylistItem.d.ts create mode 100644 typings/lib/parser/youtube/others/GridVideoItem.d.ts create mode 100644 typings/lib/parser/youtube/others/NotificationItem.d.ts create mode 100644 typings/lib/parser/youtube/others/PlaylistItem.d.ts create mode 100644 typings/lib/parser/youtube/others/ShelfRenderer.d.ts create mode 100644 typings/lib/parser/youtube/others/VideoItem.d.ts create mode 100644 typings/lib/parser/youtube/search/SearchSuggestionItem.d.ts create mode 100644 typings/lib/parser/youtube/search/VideoResultItem.d.ts create mode 100644 typings/lib/parser/ytmusic/index.d.ts create mode 100644 typings/lib/parser/ytmusic/others/PlaylistItem.d.ts create mode 100644 typings/lib/parser/ytmusic/search/AlbumResultItem.d.ts create mode 100644 typings/lib/parser/ytmusic/search/ArtistResultItem.d.ts create mode 100644 typings/lib/parser/ytmusic/search/MusicSearchSuggestionItem.d.ts create mode 100644 typings/lib/parser/ytmusic/search/PlaylistResultItem.d.ts create mode 100644 typings/lib/parser/ytmusic/search/SongResultItem.d.ts create mode 100644 typings/lib/parser/ytmusic/search/TopResultItem.d.ts create mode 100644 typings/lib/parser/ytmusic/search/VideoResultItem.d.ts create mode 100644 typings/lib/proto/index.d.ts create mode 100644 typings/lib/proto/messages.d.ts create mode 100644 typings/lib/utils/Constants.d.ts create mode 100644 typings/lib/utils/Request.d.ts create mode 100644 typings/lib/utils/Utils.d.ts diff --git a/lib/parser/contents/Author.js b/lib/parser/contents/Author.js index 3e0fc177d..479a1b9a1 100644 --- a/lib/parser/contents/Author.js +++ b/lib/parser/contents/Author.js @@ -4,7 +4,14 @@ const Thumbnail = require("./Thumbnail"); class Author { #nav_text; + /** + * @type {import('./MetadataBadge')[]} + */ badges; + /** + * @type {Thumbnail[]} + */ + thumbnails; constructor(item, badges, thumbs) { this.#nav_text = new NavigatableText(item); this.badges = Array.isArray(badges) ? ResultsParser.parse(badges) : []; @@ -34,18 +41,23 @@ class Author { return this.#nav_text.endpoint.browseId; } - get url() { - return this.#nav_text.endpoint.url; - } - + /** + * @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'); } + /** + * @type {Thumbnail | undefined} + */ get best_thumbnail() { return this.thumbnails[0]; } diff --git a/lib/parser/contents/NavigationEndpoint.js b/lib/parser/contents/NavigationEndpoint.js index e4e5395f7..9e113716f 100644 --- a/lib/parser/contents/NavigationEndpoint.js +++ b/lib/parser/contents/NavigationEndpoint.js @@ -2,10 +2,8 @@ const ResultsParser = require("."); class NavigationEndpoint { type = 'NavigationEndpoint'; - url; + constructor (item) { - // TODO: safely remove this: - this.url = item.commandMetadata.webCommandMetadata.url; this.metadata = { api_url: item.commandMetadata.webCommandMetadata.api_url, url: item.commandMetadata.webCommandMetadata.url, @@ -19,9 +17,9 @@ class NavigationEndpoint { base_url: item.browseEndpoint.canonicalBaseUrl } : null; // this is the video id to navigate to - this.watchVideoId = item.watchEndpoint ? { - videoId: item.watchEndpoint.videoId, - playlistId: item.watchEndpoint.playlistId, + this.watchVideo = item.watchEndpoint ? { + video_id: item.watchEndpoint.videoId, + playlist_id: item.watchEndpoint.playlistId, index: item.watchEndpoint.index, // this is the video index in the playlist params: item.watchEndpoint.params, } : null; @@ -33,9 +31,15 @@ class NavigationEndpoint { // this is a playlist page to navigate to // but redirect and actually start playing it // see url for index (playnext and index searchParams) - this.watchPlaylistId = item.watchPlaylistEndpoint?.playlistId; + this.watchPlaylist = item.watchPlaylistEndpoint?.playlistId; // reels have their own navigation endpoint for some reason - this.watchReelId = item.reelWatchEndpoint?.videoId; + this.watchReel = item.reelWatchEndpoint ? { + video_id: item.reelWatchEndpoint.videoId, + player_params: item.reelWatchEndpoint.playerParams, + params: item.reelWatchEndpoint.params, + sequence_provider: item.reelWatchEndpoint.sequenceProvider, + sequence_params: item.reelWatchEndpoint.sequenceParams, + } : null; // external url this.url = item.urlEndpoint ? { url: new URL(item.urlEndpoint.url), diff --git a/lib/parser/contents/Thumbnail.js b/lib/parser/contents/Thumbnail.js index 27b7af3b0..fe8f8b2e5 100644 --- a/lib/parser/contents/Thumbnail.js +++ b/lib/parser/contents/Thumbnail.js @@ -1,10 +1,27 @@ class Thumbnail { + /** + * @type {string} + */ + url; + /** + * @type {number} + */ + width; + /** + * @type {number} + */ + height; constructor ({ url, width, height }) { this.url = url; this.width = width; this.height = height; } + /** + * Get thumbnails from response object + * @param {*} response response object + * @returns {Thumbnail[]} sorted array of thumbnails + */ static fromResponse({ thumbnails }) { if (!thumbnails) { return; diff --git a/lib/parser/contents/Video.js b/lib/parser/contents/Video.js index 3c858708c..a6c4fa3d3 100644 --- a/lib/parser/contents/Video.js +++ b/lib/parser/contents/Video.js @@ -6,6 +6,62 @@ const Thumbnail = require("./Thumbnail"); class Video { type = 'Video'; + author; + /** + * @type {import('./MetadataBadge')[]} + */ + badges; + /** + * @type {Thumbnail[]} + */ + thumbnails; + /** + * @type {import('./MovingThumbnail') | undefined} + */ + rich_thumbnail; + /** + * @type {Date | undefined} + */ + upcoming; + /** + * @type {string} + */ + id; + /** + * @type {Text} + */ + title; + /** + * @type {string} + */ + duration; + /** + * @type {Text} + */ + published_at; + /** + * @type {Text} + */ + views; + /** + * @type {{ + * text: Text, + * hoverText: Text, + * }[]} + */ + snippets; + /** + * @type {Text} + */ + description_snippet; + /** + * @type {Text} + */ + short_view_count; + /** + * @type {NavigationEndpoint} + */ + endpoint; constructor(item) { this.author = new Author(item.ownerText, item.ownerBadges, item.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail); @@ -13,43 +69,58 @@ class Video { // TODO: verify this works this.thumbnails = Thumbnail.fromResponse(item.thumbnail); this.rich_thumbnail = item.richThumbnail && ResultsParser.parseItem(item.richThumbnail); - const upcoming = item.upcomingEventData ? Number(`${item.upcomingEventData.startTime}000`) : null; + const upcoming = item.upcomingEventData && Number(`${item.upcomingEventData.startTime}000`); if (upcoming) this.upcoming = new Date(upcoming); this.id = item.videoId; this.title = new Text(item.title, ''); const lengthAlt = item.thumbnailOverlays.find(overlay => overlay.hasOwnProperty('thumbnailOverlayTimeStatusRenderer'))?.thumbnailOverlayTimeStatusRenderer; - this.duration = item.lengthText ? new Text(item.lengthText, '') : lengthAlt?.text ? new Text(lengthAlt.text).toString() : ''; + this.duration = item.lengthText ? new Text(item.lengthText, '').toString() : lengthAlt?.text ? new Text(lengthAlt.text).toString() : ''; this.published_at = new Text(item.publishedTimeText, ''); this.views = new Text(item.viewCountText, ''); // TODO: might be simplified? this appears to only contain the description this.snippets = item?.detailedMetadataSnippets?.map(snip => ({ text: new Text(snip.snippetText, ''), hoverText: new Text(snip.snippetHoverText, ''), - })); + })) || []; this.description_snippet = item.descriptionSnippet ? new Text(item.descriptionSnippet, '') : null; this.short_view_count = new Text(item.shortViewCountText, ''); this.endpoint = new NavigationEndpoint(item.navigationEndpoint); } + /** + * @returns {string} + */ get description() { - if (this.snippets) { + if (this.snippets.length > 0) { return this.snippets.map(snip => snip.text.toString()).join('') } return this.description_snippet?.toString() || ''; } + /** + * @type {boolean} + */ get is_live() { return this.badges.some(badge => badge.style === 'BADGE_STYLE_TYPE_LIVE_NOW'); } + /** + * @type {boolean} + */ get is_upcoming() { return this.upcoming && this.upcoming > new Date(); } + /** + * @type {boolean} + */ get has_captions() { return this.badges.some(badge => badge.label === 'CC'); } + /** + * @type {Thumbnail | undefined} + */ get best_thumbnail() { return this.thumbnails[0]; } diff --git a/package.json b/package.json index 41be93b68..d746c9efc 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "node": ">=14" }, "scripts": { - "test": "node test" + "test": "jest", + "types": "npx tsc" }, "types": "./typings/index.d.ts", "directories": { @@ -52,5 +53,8 @@ "like", "api", "dl" - ] + ], + "devDependencies": { + "typescript": "^4.6.4" + } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..3dc714063 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "allowJs": true, + "outDir": "./typings" + }, + "include": [ + "./lib/**/*.js", + "./index.js" + ], + "exclude": [ + "node_modules", + "**/*.d.ts" + ] +} \ No newline at end of file diff --git a/typings/index.d.ts b/typings/index.d.ts index 76425d6b7..810e6c1d3 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,190 +1,2 @@ -interface AuthInfo { - access_token: string; - refresh_token: string; - expires: Date; -} - -interface AccountInfo { - name: string; - photo: Record[]; - country: string; - language: string; -} - -interface SearchOptions { - client: 'YTMUSIC' | 'YOUTUBE'; - period: 'any' | 'hour' | 'day' | 'week' | 'month' | 'year'; - order: 'relevance' | 'rating' | 'age' | 'views'; - duration: 'any' | 'short' | 'long'; -} - -interface YouTubeSearch { - query: string; - corrected_query: string; - estimated_results: number; - videos: any[]; - getContinuation: () => Promise; -} - -interface YouTubeMusicSearch { - query: string; - corrected_query: string; - results: { - top_result?: any[]; - songs?: any[]; - albums?: any[]; - videos?: any[]; - community_playlists?: any[]; - artists?: any[]; - } -} - -type SearchResults = YouTubeSearch | YouTubeMusicSearch; - -type ClientOption = Pick; - -type SortBy = 'TOP_COMMENTS' | 'NEWEST_FIRST'; - -interface Suggestion { - text: string; - bold_text: string; -} - -interface ApiStatus { - success: boolean; - status_code: number; - data: object; - message?: string; -} - -interface Comments { - page_count: number, - comment_count: number; - items: any[]; - getContinuation: () => Promise; -} - -interface Video { - title: string; - description: string; - thumbnail: object; - metadata: Record; - like: () => Promise; - dislike: () => Promise; - removeLike: () => Promise; - subscribe: () => Promise; - unsubscribe: () => Promise; - comment: (text: string) => Promise; - getComments: () => Promise; - getLivechat: () => any; // TODO type LiveChat - setNotificationPreferences: () => Promise; -} - -interface Channel { - title: string; - description: string; - metadata: object; - content: object; -} - -interface PlayList { - description: string; - items: any[]; - title: string; - total_items: string | number; - duration?: string; - last_updated?: string; - views?: string; - year?: string; -} - -interface History { - items: { - date: string; - videos: any[]; - }[]; - getContinuation: () => Promise; -} - -interface SubscriptionFeed { - items: { - date: string; - videos: any[]; - }[]; - getContinuation: () => Promise; -} - -interface HomeFeed { - videos: { - id: string; - title: string; - description: string; - channel: string; - metadata: Record; - }[]; - getContinuation: () => Promise; -} - -interface Trending { - now: { - content: { - title: string; - videos: []; - }[]; - }; - music: { getVideos: () => Promise; }; - gaming: { getVideos: () => Promise; }; - movies: { getVideos: () => Promise; }; -} - -interface Notifications { - items: { - title: string; - sent_time: string; - channel_name: string; - channel_thumbnail: Record; - video_thumbnail: Record; - video_url: string; - read: boolean; - notification_id: string; - }[]; - getContinuation: () => Promise; -} - -interface StreamingData { - selected_format: Record; - formats: any[]; -} -interface StreamingOptions { - quality?: string; - type?: string; - format?: string; -} - -interface Config { - gl?: string; - cookie?: string; -} - -export default class Innertube { - constructor(auth_info?: Config) - - public signIn(auth_info: AuthInfo): Promise; - public signOut(): Promise; - public getAccountInfo(): Promise; - public search(query: string, options: SearchOptions): Promise; - public getSearchSuggestions(query: string, options?: ClientOption): Promise; - public getDetails(video_id: string): Promise; - public getChannel(id: string): Promise; - public getLyrics(video_id: string): Promise; - public getPlaylist(playlist_id: string, options?: ClientOption): Promise; - public getComments(video_id: string, sort_by?: SortBy): Promise; - public getHistory(): Promise; - public getHomeFeed(): Promise; - public getTrending(): Promise; - public getSubscriptionsFeed(): Promise; - public getNotifications(): Promise; - public getUnseenNotificationsCount(): Promise; - public getStreamingData(id: string, options?: StreamingOptions): Promise; - public download(id: string, options?: StreamingOptions): ReadableStream; -} \ No newline at end of file +declare const _exports: typeof import("./lib/Innertube"); +export = _exports; diff --git a/typings/lib/Innertube.d.ts b/typings/lib/Innertube.d.ts new file mode 100644 index 000000000..a5389d876 --- /dev/null +++ b/typings/lib/Innertube.d.ts @@ -0,0 +1,511 @@ +export = Innertube; +declare class Innertube { + /** + * ```js + * const Innertube = require('youtubei.js'); + * const youtube = await new Innertube(); + * ``` + * @param {object} [config] + * @param {string} [config.gl] + * @param {string} [config.cookie] + * @returns {Innertube} + * @constructor + */ + constructor(config?: { + gl?: string; + cookie?: string; + }); + config: { + gl?: string; + cookie?: string; + }; + key: any; + version: any; + context: any; + player_url: any; + logged_in: any; + sts: any; + /** + * @event Innertube#auth - Fired when signing in to an account. + * @event Innertube#update-credentials - Fired when the access token is no longer valid. + * @type {EventEmitter} + */ + ev: EventEmitter; + auth_apisid: any; + request: Request; + account: { + info: () => Promise<{ + name: string; + photo: Array; + country: string; + language: string; + }>; + settings: { + notifications: { + /** + * Notify about activity from the channels you're subscribed to. + * + * @param {boolean} new_value + * @returns {Promise.<{ success: boolean; status_code: string; }>} + */ + setSubscriptions: (new_value: boolean) => Promise<{ + success: boolean; + status_code: string; + }>; + /** + * Recommended content notifications. + * + * @param {boolean} new_value + * @returns {Promise.<{ success: boolean; status_code: string; }>} + */ + setRecommendedVideos: (new_value: boolean) => Promise<{ + success: boolean; + status_code: string; + }>; + /** + * Notify about activity on your channel. + * + * @param {boolean} new_value + * @returns {Promise.<{ success: boolean; status_code: string; }>} + */ + setChannelActivity: (new_value: boolean) => Promise<{ + success: boolean; + status_code: string; + }>; + /** + * Notify about replies to your comments. + * + * @param {boolean} new_value + * @returns {Promise.<{ success: boolean; status_code: string; }>} + */ + setCommentReplies: (new_value: boolean) => Promise<{ + success: boolean; + status_code: string; + }>; + /** + * Notify when others mention your channel. + * + * @param {boolean} new_value + * @returns {Promise.<{ success: boolean; status_code: string; }>} + */ + setMentions: (new_value: boolean) => Promise<{ + success: boolean; + status_code: string; + }>; + /** + * Notify when others share your content on their channels. + * + * @param {boolean} new_value + * @returns {Promise.<{ success: boolean; status_code: string; }>} + */ + setSharedContent: (new_value: boolean) => Promise<{ + success: boolean; + status_code: string; + }>; + }; + privacy: { + /** + * If set to true, your subscriptions won't be visible to others. + * + * @param {boolean} new_value + * @returns {Promise.<{ success: boolean; status_code: string; }>} + */ + setSubscriptionsPrivate: (new_value: boolean) => Promise<{ + success: boolean; + status_code: string; + }>; + /** + * If set to true, saved playlists won't appear on your channel. + * + * @param {boolean} new_value + * @returns {Promise.<{ success: boolean; status_code: string; }>} + */ + setSavedPlaylistsPrivate: (new_value: boolean) => Promise<{ + success: boolean; + status_code: string; + }>; + }; + }; + }; + interact: { + /** + * Likes a given video. + * + * @param {string} video_id + * @returns {Promise.<{ success: boolean; status_code: string; }>} + */ + like: (video_id: string) => Promise<{ + success: boolean; + status_code: string; + }>; + /** + * Diskes a given video. + * + * @param {string} video_id + * @returns {Promise.<{ success: boolean; status_code: string; }>} + */ + dislike: (video_id: string) => Promise<{ + success: boolean; + status_code: string; + }>; + /** + * Removes a like/dislike. + * + * @param {string} video_id + * @returns {Promise.<{ success: boolean; status_code: string; }>} + */ + removeLike: (video_id: string) => Promise<{ + success: boolean; + status_code: string; + }>; + /** + * Posts a comment on a given video. + * + * @param {string} video_id + * @param {string} text + * @returns {Promise.<{ success: boolean; status_code: string; }>} + */ + comment: (video_id: string, text: string) => Promise<{ + success: boolean; + status_code: string; + }>; + /** + * Subscribes to a given channel. + * + * @param {string} channel_id + * @returns {Promise.<{ success: boolean; status_code: string; }>} + */ + subscribe: (channel_id: string) => Promise<{ + success: boolean; + status_code: string; + }>; + /** + * Unsubscribes from a given channel. + * + * @param {string} channel_id + * @returns {Promise.<{ success: boolean; status_code: string; }>} + */ + unsubscribe: (channel_id: string) => Promise<{ + success: boolean; + status_code: string; + }>; + /** + * Changes notification preferences for a given channel. + * Only works with channels you are subscribed to. + * + * @param {string} channel_id + * @param {string} type PERSONALIZED | ALL | NONE + * @returns {Promise.<{ success: boolean; status_code: string; }>} + */ + setNotificationPreferences: (channel_id: string, type: string) => Promise<{ + success: boolean; + status_code: string; + }>; + }; + playlist: { + /** + * Creates a playlist. + * + * @param {string} title + * @param {string} video_ids + * @returns {Promise.<{ success: boolean; status_code: string; playlist_id: string; }>} + */ + create: (title: string, video_ids: string) => Promise<{ + success: boolean; + status_code: string; + playlist_id: string; + }>; + /** + * Deletes a given playlist. + * + * @param {string} playlist_id + * @returns {Promise.<{ success: boolean; status_code: string; playlist_id: string; }>} + */ + delete: (playlist_id: string) => Promise<{ + success: boolean; + status_code: string; + playlist_id: string; + }>; + /** + * Adds an array of videos to a given playlist. + * + * @param {string} playlist_id + * @param {Array.} video_ids + * @returns {Promise.<{ success: boolean; status_code: string; playlist_id: string; }>} + */ + addVideos: (playlist_id: string, video_ids: Array) => Promise<{ + success: boolean; + status_code: string; + playlist_id: string; + }>; + /** + * Removes videos from a given playlist. + * + * @param {string} playlist_id + * @param {Array.} video_ids + * @returns {Promise.<{ success: boolean; status_code: string; playlist_id: string; }>} + */ + removeVideos: (playlist_id: string, video_ids: Array) => Promise<{ + success: boolean; + status_code: string; + playlist_id: string; + }>; + }; + /** + * Signs-in to a google account. + * + * @param {object} auth_info + * @param {string} auth_info.access_token - Token used to sign in. + * @param {string} auth_info.refresh_token - Token used to get a new access token. + * @param {Date} auth_info.expires - Access token's expiration date, which is usually 24hrs-ish + * @returns {Promise.} + */ + signIn(auth_info?: { + access_token: string; + refresh_token: string; + expires: Date; + }): Promise; + access_token: any; + refresh_token: any; + /** + * Signs out of your account. + * @returns {Promise.<{ success: boolean; status_code: number }>} + */ + signOut(): Promise<{ + success: boolean; + status_code: number; + }>; + /** + * Retrieves account details. + * @returns {Promise.<{ name: string; photo: Array; country: string; language: string; }>} + */ + getAccountInfo(): Promise<{ + name: string; + photo: Array; + country: string; + language: string; + }>; + /** + * Searches on YouTube. + * + * @param {string} query - Search query. + * @param {object} options - Search options. + * @param {string} option.params - Pre-defined search parameter. + * @param {string} options.client - Client used to perform the search, can be: `YTMUSIC` or `YOUTUBE`. + * @param {string} options.period - Filter videos uploaded within a period, can be: any | hour | day | week | month | year + * @param {string} options.order - Filter results by order, can be: relevance | rating | age | views + * @param {string} options.duration - Filter video results by duration, can be: any | short | long + * @returns {Promise.<{ query: string; corrected_query: string; estimated_results: number; videos: [] } | + * { results: { songs: []; videos: []; albums: []; community_playlists: [] } }>} + */ + search(query: string, options?: object): Promise<{ + query: string; + corrected_query: string; + estimated_results: number; + videos: []; + } | { + results: { + songs: []; + videos: []; + albums: []; + community_playlists: []; + }; + }>; + /** + * Retrieves search suggestions. + * + * @param {string} input - The search query. + * @param {object} [options] - Search options. + * @param {string} [options.client='YOUTUBE'] - Client used to retrieve search suggestions, can be: `YOUTUBE` or `YTMUSIC`. + * @returns {Promise.<[{ text: string; bold_text: string }]>} + */ + getSearchSuggestions(input: string, options?: { + client?: string; + }): Promise<[{ + text: string; + bold_text: string; + }]>; + /** + * Retrieves video info. + * + * @param {string} video_id - Video id + * @return {Promise.<{ title: string; description: string; thumbnail: []; metadata: object }>} + */ + getDetails(video_id: string): Promise<{ + title: string; + description: string; + thumbnail: []; + metadata: object; + }>; + /** + * Retrieves comments for a video. + * + * @param {string} video_id - Video id + * @param {string} [sort_by] - Can be: `TOP_COMMENTS` or `NEWEST_FIRST`. + * @return {Promise.<{ page_count: number; comment_count: number; items: []; }>} + */ + getComments(video_id: string, sort_by?: string): Promise<{ + page_count: number; + comment_count: number; + items: []; + }>; + /** + * Retrieves contents for a given channel. (WIP) + * + * @param {string} id - The id of the channel. + * @return {Promise.<{ title: string; description: string; metadata: object; content: object }>} + */ + getChannel(id: string): Promise<{ + title: string; + description: string; + metadata: object; + content: object; + }>; + /** + * Retrieves your watch history. + * @returns {Promise.<{ items: [{ date: string; videos: [] }] }>} + */ + getHistory(): Promise<{ + items: [{ + date: string; + videos: []; + }]; + }>; + /** + * Retrieves YouTube's home feed (aka recommendations). + * @returns {Promise.<{ videos: [{ id: string; title: string; description: string; channel: string; metadata: object }] }>} + */ + getHomeFeed(): Promise<{ + videos: [{ + id: string; + title: string; + description: string; + channel: string; + metadata: object; + }]; + }>; + /** + * Retrieves trending content. + * @returns {Promise.<{ now: { content: [{ title: string; videos: []; }] }; + * music: { getVideos: Promise.; }; gaming: { getVideos: Promise.; }; + * gaming: { getVideos: Promise.; }; }>} + */ + getTrending(): Promise<{ + now: { + content: [{ + title: string; + videos: []; + }]; + }; + music: { + getVideos: Promise; + }; + gaming: { + getVideos: Promise; + }; + gaming: { + getVideos: Promise; + }; + }>; + /** + * Retrieves your subscriptions feed. + * @returns {Promise.<{ items: [{ date: string; videos: [] }] }>} + */ + getSubscriptionsFeed(): Promise<{ + items: [{ + date: string; + videos: []; + }]; + }>; + /** + * Retrieves your notifications. + * @returns {Promise.<{ items: [{ title: string; sent_time: string; channel_name: string; channel_thumbnail: {}; video_thumbnail: {}; video_url: string; read: boolean; notification_id: string }] }>} + */ + getNotifications(): Promise<{ + items: [{ + title: string; + sent_time: string; + channel_name: string; + channel_thumbnail: {}; + video_thumbnail: {}; + video_url: string; + read: boolean; + notification_id: string; + }]; + }>; + /** + * Retrieves unseen notifications count. + * @returns {Promise.} unseen notifications count. + */ + getUnseenNotificationsCount(): Promise; + /** + * Retrieves lyrics for a given song if available. + * + * @param {string} video_id + * @returns {Promise.} Song lyrics + */ + getLyrics(video_id: string): Promise; + /** + * Parses a given playlist. + * + * @param {string} playlist_id - The id of the playlist. + * @param {object} options - { client: 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: [] } | + * { title: string; description: string; total_items: number; duration: string; year: string; items: [] }>} + */ + getPlaylist(playlist_id: string, options?: { + client: string; + }): Promise<{ + title: string; + description: string; + total_items: string; + last_updated: string; + views: string; + items: []; + } | { + title: string; + description: string; + total_items: number; + duration: string; + year: string; + items: []; + }>; + /** + * An alternative to {@link download}. + * Returns deciphered streaming data. + * + * @param {string} id - Video id + * @param {object} options - Download options. + * @param {string} options.quality - Video quality; 360p, 720p, 1080p, etc.... + * @param {string} options.type - Download type, can be: video, audio or videoandaudio + * @param {string} options.format - File format + * @returns {Promise.<{ selected_format: {}; formats: [] }>} + */ + getStreamingData(id: string, options?: { + quality: string; + type: string; + format: string; + }): Promise<{ + selected_format: {}; + formats: []; + }>; + /** + * Downloads a given video. If you only need the direct download link take a look at {@link getStreamingData}. + * + * @param {string} id - Video id + * @param {object} options - Download options. + * @param {string} options.quality - Video quality; 360p, 720p, 1080p, etc.... + * @param {string} options.type - Download type, can be: video, audio or videoandaudio + * @param {string} options.format - File format + * @return {ReadableStream} + */ + download(id: string, options?: { + quality: string; + type: string; + format: string; + }): ReadableStream; + #private; +} +import Request = require("./utils/Request"); diff --git a/typings/lib/core/Actions.d.ts b/typings/lib/core/Actions.d.ts new file mode 100644 index 000000000..e2e8856ca --- /dev/null +++ b/typings/lib/core/Actions.d.ts @@ -0,0 +1,157 @@ +/** + * Performs direct interactions on YouTube. + * + * @param {Innertube} session + * @param {string} engagement_type + * @param {object} args + * @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>} + */ +export function engage(session: Innertube, engagement_type: string, args?: object): Promise<{ + success: boolean; + status_code: number; + data: object; + message?: string; +}>; +/** + * Accesses YouTube's various sections. + * + * @param {Innertube} session + * @param {string} action + * @param {object} args + * @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>} + */ +export function browse(session: Innertube, action: string, args?: object): Promise<{ + success: boolean; + status_code: number; + data: object; + message?: string; +}>; +/** + * Account settings endpoints. + * + * @param {Innertube} session + * @param {string} action + * @param {object} args + * @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>} + */ +export function account(session: Innertube, action: string, args?: object): Promise<{ + success: boolean; + status_code: number; + data: object; + message?: string; +}>; +export function playlist(session: any, action: any, args?: {}): Promise<{ + success: boolean; + status_code: any; + message: string; + data?: undefined; +} | { + success: boolean; + status_code: any; + data: any; + message?: undefined; +}>; +/** + * Endpoints used to report content. + * + * @param {Innertube} session + * @param {string} action + * @param {object} args + * @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>} + */ +export function flag(session: Innertube, action: string, args?: object): Promise<{ + success: boolean; + status_code: number; + data: object; + message?: string; +}>; +/** + * Accesses YouTube Music endpoints (/youtubei/v1/music/). + * + * @param {Innertube} session + * @param {string} action + * @param {object} args + * @todo Implement more endpoints. + * @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>} + */ +export function music(session: Innertube, action: string, args: object): Promise<{ + success: boolean; + status_code: number; + data: object; + message?: string; +}>; +/** + * Searches a given query on YouTube/YTMusic. + * + * @param {Innertube} session + * @param {string} client - YouTube client: YOUTUBE | YTMUSIC + * @param {object} args - Search arguments. + * @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>} + */ +export function search(session: Innertube, client: string, args?: object): Promise<{ + success: boolean; + status_code: number; + data: object; + message?: string; +}>; +/** + * Interacts with YouTube's notification system. + * + * @param {Innertube} session + * @param {string} action + * @param {object} args + * @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>} + */ +export function notifications(session: Innertube, action: string, args?: object): Promise<{ + success: boolean; + status_code: number; + data: object; + message?: string; +}>; +/** + * Interacts with YouTube's livechat system. + * + * @param {Innertube} session + * @param {string} action + * @param {object} args + * @returns {Promise.<{ success: boolean; data: object; message?: string }>} + */ +export function livechat(session: Innertube, action: string, args?: object): Promise<{ + success: boolean; + data: object; + message?: string; +}>; +/** + * Retrieves video data. + * + * @param {Innertube} session + * @param {object} args + * @returns {Promise.} - Video data. + */ +export function getVideoInfo(session: Innertube, args?: object): Promise; +/** + * Requests continuation for previously performed actions. + * + * @param {Innertube} session + * @param {object} args + * @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>} + */ +export function next(session: Innertube, args?: object): Promise<{ + success: boolean; + status_code: number; + data: object; + message?: string; +}>; +/** + * Gets search suggestions. + * + * @param {Innertube} session + * @param {string} query + * @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>} + */ +export function getSearchSuggestions(session: Innertube, client: any, input: any): Promise<{ + success: boolean; + status_code: number; + data: object; + message?: string; +}>; diff --git a/typings/lib/core/HomeFeed.d.ts b/typings/lib/core/HomeFeed.d.ts new file mode 100644 index 000000000..ef42b8c0a --- /dev/null +++ b/typings/lib/core/HomeFeed.d.ts @@ -0,0 +1,22 @@ +export = HomeFeed; +declare class HomeFeed { + constructor(data: any); + /** + * Get the original page data + */ + get page(): { + contents: any; + }; + /** + * Get all the videos in the home feed + * @returns {Array} + */ + getVideos(): Array; + /** + * Get all the videos in the home feed + * @deprecated Use getVideos instead + */ + get videos(): any; + getContinuation(): void; + #private; +} diff --git a/typings/lib/core/Livechat.d.ts b/typings/lib/core/Livechat.d.ts new file mode 100644 index 000000000..8b9f8a952 --- /dev/null +++ b/typings/lib/core/Livechat.d.ts @@ -0,0 +1,48 @@ +export = Livechat; +declare class Livechat { + constructor(session: any, token: any, channel_id: any, video_id: any); + ctoken: any; + session: any; + video_id: any; + channel_id: any; + message_queue: any[]; + id_cache: any[]; + poll_intervals_ms: number; + running: boolean; + metadata_ctoken: any; + livechat_poller: number; + sendMessage(text: any): Promise<{ + success: boolean; + data: any; + message?: string; + } | { + success: boolean; + status_code: any; + deleteMessage: () => Promise<{ + success: boolean; + data: any; + message?: string; + } | { + success: boolean; + status_code: any; + }>; + message_data: { + text: any; + author: { + name: any; + channel_id: any; + profile_picture: any; + }; + timestamp: any; + id: any; + }; + }>; + /** + * Blocks a user. + * @todo Implement this method. + * @param {object} msg_params + */ + blockUser(msg_params: object): Promise; + stop(): void; + #private; +} diff --git a/typings/lib/core/OAuth.d.ts b/typings/lib/core/OAuth.d.ts new file mode 100644 index 000000000..a87b08ad1 --- /dev/null +++ b/typings/lib/core/OAuth.d.ts @@ -0,0 +1,35 @@ +export = OAuth; +declare class OAuth { + constructor(ev: any); + /** + * Starts the auth flow in case no valid credentials are available. + * @returns {Promise.} + */ + init(auth_info: any): Promise; + client_id: string; + client_secret: string; + refresh_interval: any; + /** + * Refreshes the access token if necessary. + * @returns {Promise.} + */ + checkTokenValidity(): Promise; + /** + * Revokes access token (note that the refresh token will also be revoked). + * @returns {Promise.} + */ + revokeAccessToken(): Promise; + getAccessToken(): any; + getRefreshToken(): any; + /** + * Checks if the auth info is valid. + * @returns {boolean} true | false + */ + isValidAuthInfo(): boolean; + /** + * Checks access token validity. + * @returns {boolean} true | false + */ + shouldRefreshToken(): boolean; + #private; +} diff --git a/typings/lib/core/Player.d.ts b/typings/lib/core/Player.d.ts new file mode 100644 index 000000000..fb260f8d2 --- /dev/null +++ b/typings/lib/core/Player.d.ts @@ -0,0 +1,11 @@ +export = Player; +declare class Player { + constructor(session: any); + session: any; + player_name: string; + tmp_cache_dir: string; + init(): Promise; + sig_decipher_sc: string; + ntoken_sc: string; + #private; +} diff --git a/typings/lib/deciphers/NToken.d.ts b/typings/lib/deciphers/NToken.d.ts new file mode 100644 index 000000000..71678ebd9 --- /dev/null +++ b/typings/lib/deciphers/NToken.d.ts @@ -0,0 +1,12 @@ +export = NToken; +declare class NToken { + constructor(raw_code: any, n: any); + n: any; + raw_code: any; + /** + * Solves throttling challange by transforming the n token. + * @returns {string} transformed token. + */ + transform(): string; + #private; +} diff --git a/typings/lib/deciphers/Signature.d.ts b/typings/lib/deciphers/Signature.d.ts new file mode 100644 index 000000000..65fa00599 --- /dev/null +++ b/typings/lib/deciphers/Signature.d.ts @@ -0,0 +1,13 @@ +export = Signature; +declare class Signature { + constructor(url: any, player: any); + url: any; + player: any; + func_regex: RegExp; + actions_regex: RegExp; + /** + * Deciphers signature. + */ + decipher(): string; + #private; +} diff --git a/typings/lib/parser/contents/Author.d.ts b/typings/lib/parser/contents/Author.d.ts new file mode 100644 index 000000000..e0ebd52bd --- /dev/null +++ b/typings/lib/parser/contents/Author.d.ts @@ -0,0 +1,30 @@ +export = Author; +declare class Author { + constructor(item: any, badges: any, thumbs: any); + /** + * @type {import('./MetadataBadge')[]} + */ + badges: import('./MetadataBadge')[]; + /** + * @type {Thumbnail[]} + */ + thumbnails: Thumbnail[]; + set name(arg: string); + get name(): string; + get endpoint(): import("./NavigationEndpoint"); + get id(): any; + /** + * @type {boolean} + */ + get is_verified(): boolean; + /** + * @type {boolean} + */ + get is_verified_artist(): boolean; + /** + * @type {Thumbnail | undefined} + */ + get best_thumbnail(): Thumbnail; + #private; +} +import Thumbnail = require("./Thumbnail"); diff --git a/typings/lib/parser/contents/BackstageImage.d.ts b/typings/lib/parser/contents/BackstageImage.d.ts new file mode 100644 index 000000000..3c4f60437 --- /dev/null +++ b/typings/lib/parser/contents/BackstageImage.d.ts @@ -0,0 +1,7 @@ +export = BackstageImage; +declare class BackstageImage { + constructor(item: any); + type: string; + image: Thumbnail[]; +} +import Thumbnail = require("./Thumbnail"); diff --git a/typings/lib/parser/contents/BackstagePost.d.ts b/typings/lib/parser/contents/BackstagePost.d.ts new file mode 100644 index 000000000..3961a86ed --- /dev/null +++ b/typings/lib/parser/contents/BackstagePost.d.ts @@ -0,0 +1,15 @@ +export = BackstagePost; +declare class BackstagePost { + constructor(item: any); + type: string; + id: any; + author: Author; + content: Text; + published_at: Text; + likes: Text; + actions: any; + attachment: any; + get endpoint(): any; +} +import Author = require("./Author"); +import Text = require("./Text"); diff --git a/typings/lib/parser/contents/BackstagePostThread.d.ts b/typings/lib/parser/contents/BackstagePostThread.d.ts new file mode 100644 index 000000000..73d48fd10 --- /dev/null +++ b/typings/lib/parser/contents/BackstagePostThread.d.ts @@ -0,0 +1,6 @@ +export = BackstagePostThread; +declare class BackstagePostThread { + constructor(item: any); + type: string; + post: any; +} diff --git a/typings/lib/parser/contents/Boilerplate.d.ts b/typings/lib/parser/contents/Boilerplate.d.ts new file mode 100644 index 000000000..544786a5e --- /dev/null +++ b/typings/lib/parser/contents/Boilerplate.d.ts @@ -0,0 +1,5 @@ +export = MetadataBadge; +declare class MetadataBadge { + constructor(item: any); + type: string; +} diff --git a/typings/lib/parser/contents/Button.d.ts b/typings/lib/parser/contents/Button.d.ts new file mode 100644 index 000000000..a84d6f530 --- /dev/null +++ b/typings/lib/parser/contents/Button.d.ts @@ -0,0 +1,12 @@ +export = Button; +declare class Button { + constructor(item: any); + type: string; + navigation_endpoint: NavigationEndpoint; + service_endpoint: NavigationEndpoint; + text: Text; + tooltip: any; + get endpoint(): NavigationEndpoint; +} +import NavigationEndpoint = require("./NavigationEndpoint"); +import Text = require("./Text"); diff --git a/typings/lib/parser/contents/Channel.d.ts b/typings/lib/parser/contents/Channel.d.ts new file mode 100644 index 000000000..f5f3d5f08 --- /dev/null +++ b/typings/lib/parser/contents/Channel.d.ts @@ -0,0 +1,14 @@ +export = Channel; +declare class Channel { + constructor(item: any); + type: string; + id: any; + author: Author; + subscribers: Text; + description_snippet: Text; + videos: Text; + endpoint: NavigationEndpoint; +} +import Author = require("./Author"); +import Text = require("./Text"); +import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/ChannelAboutFullMetadata.d.ts b/typings/lib/parser/contents/ChannelAboutFullMetadata.d.ts new file mode 100644 index 000000000..5612738c5 --- /dev/null +++ b/typings/lib/parser/contents/ChannelAboutFullMetadata.d.ts @@ -0,0 +1,17 @@ +export = ChannelAboutFullMetadata; +declare class ChannelAboutFullMetadata { + constructor(item: any); + type: string; + id: any; + canonical_channel_url: any; + author: Author; + views: Text; + joined: Text; + description: Text; + email_reveal: NavigationEndpoint; + can_reveal_email: boolean; + country: Text; +} +import Author = require("./Author"); +import Text = require("./Text"); +import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/ChannelMetadata.d.ts b/typings/lib/parser/contents/ChannelMetadata.d.ts new file mode 100644 index 000000000..8783c37b2 --- /dev/null +++ b/typings/lib/parser/contents/ChannelMetadata.d.ts @@ -0,0 +1,21 @@ +export = ChannelMetadata; +declare class ChannelMetadata { + constructor(item: any); + type: string; + title: any; + description: any; + metadata: { + url: any; + rss_urls: any; + vanity_channel_url: any; + external_id: any; + is_family_safe: any; + keywords: any; + avatar: Thumbnail[]; + available_countries: any; + android_deep_link: any; + android_appindexing_link: any; + ios_appindexing_link: any; + }; +} +import Thumbnail = require("./Thumbnail"); diff --git a/typings/lib/parser/contents/ChannelVideoPlayer.d.ts b/typings/lib/parser/contents/ChannelVideoPlayer.d.ts new file mode 100644 index 000000000..0d60556a5 --- /dev/null +++ b/typings/lib/parser/contents/ChannelVideoPlayer.d.ts @@ -0,0 +1,11 @@ +export = ChannelVideoPlayer; +declare class ChannelVideoPlayer { + constructor(item: any); + type: string; + id: any; + title: Text; + description: Text; + views: Text; + published_at: Text; +} +import Text = require("./Text"); diff --git a/typings/lib/parser/contents/ChildVideo.d.ts b/typings/lib/parser/contents/ChildVideo.d.ts new file mode 100644 index 000000000..8f7ad9f20 --- /dev/null +++ b/typings/lib/parser/contents/ChildVideo.d.ts @@ -0,0 +1,11 @@ +export = ChildVideo; +declare class ChildVideo { + constructor(item: any); + type: string; + id: any; + title: Text; + length: Text; + endpoint: NavigationEndpoint; +} +import Text = require("./Text"); +import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/ChipCloudChip.d.ts b/typings/lib/parser/contents/ChipCloudChip.d.ts new file mode 100644 index 000000000..45b1c1a1a --- /dev/null +++ b/typings/lib/parser/contents/ChipCloudChip.d.ts @@ -0,0 +1,10 @@ +export = ChipCloudChip; +declare class ChipCloudChip { + constructor(item: any); + type: string; + selected: any; + endpoint: NavigationEndpoint; + text: Text; +} +import NavigationEndpoint = require("./NavigationEndpoint"); +import Text = require("./Text"); diff --git a/typings/lib/parser/contents/CollageHeroImage.d.ts b/typings/lib/parser/contents/CollageHeroImage.d.ts new file mode 100644 index 000000000..8dc0a5b7c --- /dev/null +++ b/typings/lib/parser/contents/CollageHeroImage.d.ts @@ -0,0 +1,11 @@ +export = CollageHeroImage; +declare class CollageHeroImage { + constructor(item: any); + type: string; + left: Thumbnail[]; + top_right: Thumbnail[]; + bottom_right: Thumbnail[]; + endpoint: NavigationEndpoint; +} +import Thumbnail = require("./Thumbnail"); +import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/CommentActionButtons.d.ts b/typings/lib/parser/contents/CommentActionButtons.d.ts new file mode 100644 index 000000000..b35cd0623 --- /dev/null +++ b/typings/lib/parser/contents/CommentActionButtons.d.ts @@ -0,0 +1,8 @@ +export = CommentActionButtons; +declare class CommentActionButtons { + constructor(item: any); + type: string; + like: any; + reply: any; + dislike: any; +} diff --git a/typings/lib/parser/contents/CompactVideo.d.ts b/typings/lib/parser/contents/CompactVideo.d.ts new file mode 100644 index 000000000..08289d9e0 --- /dev/null +++ b/typings/lib/parser/contents/CompactVideo.d.ts @@ -0,0 +1,18 @@ +export = CompactVideo; +declare class CompactVideo { + constructor(item: any); + type: string; + id: any; + thumbnails: Thumbnail[]; + rich_thumbnail: any; + title: Text; + author: Author; + published_at: Text; + views: Text; + duration: Text; + endpoint: NavigationEndpoint; +} +import Thumbnail = require("./Thumbnail"); +import Text = require("./Text"); +import Author = require("./Author"); +import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/ContinuationItem.d.ts b/typings/lib/parser/contents/ContinuationItem.d.ts new file mode 100644 index 000000000..b9f95c50f --- /dev/null +++ b/typings/lib/parser/contents/ContinuationItem.d.ts @@ -0,0 +1,7 @@ +export = ContinuationItem; +declare class ContinuationItem { + constructor(item: any); + type: string; + endpoint: NavigationEndpoint; +} +import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/ExpandableTab.d.ts b/typings/lib/parser/contents/ExpandableTab.d.ts new file mode 100644 index 000000000..3b9f0acf0 --- /dev/null +++ b/typings/lib/parser/contents/ExpandableTab.d.ts @@ -0,0 +1,10 @@ +export = ExpandableTab; +declare class ExpandableTab { + constructor(item: any); + type: string; + title: any; + endpoint: NavigationEndpoint; + selected: any; + content: any; +} +import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/GenericContainer.d.ts b/typings/lib/parser/contents/GenericContainer.d.ts new file mode 100644 index 000000000..a39851514 --- /dev/null +++ b/typings/lib/parser/contents/GenericContainer.d.ts @@ -0,0 +1,7 @@ +declare function _exports(name: any): { + new (item: any): { + type: any; + content: any; + }; +}; +export = _exports; diff --git a/typings/lib/parser/contents/GenericList.d.ts b/typings/lib/parser/contents/GenericList.d.ts new file mode 100644 index 000000000..cbd243401 --- /dev/null +++ b/typings/lib/parser/contents/GenericList.d.ts @@ -0,0 +1,8 @@ +declare function _exports(name: any, field?: string): { + new (items: any): { + type: any; + is_list: boolean; + contents: any; + }; +}; +export = _exports; diff --git a/typings/lib/parser/contents/GridChannel.d.ts b/typings/lib/parser/contents/GridChannel.d.ts new file mode 100644 index 000000000..37ec0b278 --- /dev/null +++ b/typings/lib/parser/contents/GridChannel.d.ts @@ -0,0 +1,14 @@ +export = GridChannel; +declare class GridChannel { + constructor(item: any); + type: string; + id: any; + thumbnails: Thumbnail[]; + videos: Text; + subscribers: Text; + name: Text; + endpoint: NavigationEndpoint; +} +import Thumbnail = require("./Thumbnail"); +import Text = require("./Text"); +import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/GridPlaylist.d.ts b/typings/lib/parser/contents/GridPlaylist.d.ts new file mode 100644 index 000000000..ee9ae3a99 --- /dev/null +++ b/typings/lib/parser/contents/GridPlaylist.d.ts @@ -0,0 +1,15 @@ +export = GridPlaylist; +declare class GridPlaylist { + constructor(item: any); + type: string; + id: any; + videos: Text; + thumbnauls: Thumbnail[]; + video_thumbnails: any; + title: Text; + endpoint: NavigationEndpoint; + view_playlist: Text; +} +import Text = require("./Text"); +import Thumbnail = require("./Thumbnail"); +import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/GridVideo.d.ts b/typings/lib/parser/contents/GridVideo.d.ts new file mode 100644 index 000000000..5c903350e --- /dev/null +++ b/typings/lib/parser/contents/GridVideo.d.ts @@ -0,0 +1,17 @@ +export = GridVideo; +declare class GridVideo { + constructor(item: any); + type: string; + id: any; + thumbnails: Thumbnail[]; + rich_thumbnail: any; + title: Text; + badges: any; + duration: string | Text; + published_at: Text; + views: Text; + endpoint: NavigationEndpoint; +} +import Thumbnail = require("./Thumbnail"); +import Text = require("./Text"); +import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/HorizontalCardList.d.ts b/typings/lib/parser/contents/HorizontalCardList.d.ts new file mode 100644 index 000000000..28133bfc4 --- /dev/null +++ b/typings/lib/parser/contents/HorizontalCardList.d.ts @@ -0,0 +1,8 @@ +export = HorizontalCardList; +declare class HorizontalCardList { + constructor(item: any); + type: string; + cards: any; + header: Text; +} +import Text = require("./Text"); diff --git a/typings/lib/parser/contents/HorizontalList.d.ts b/typings/lib/parser/contents/HorizontalList.d.ts new file mode 100644 index 000000000..55d2d069a --- /dev/null +++ b/typings/lib/parser/contents/HorizontalList.d.ts @@ -0,0 +1,7 @@ +export = HorizontalList; +declare class HorizontalList { + constructor(item: any); + type: string; + visible_item_count: any; + items: any; +} diff --git a/typings/lib/parser/contents/Menu.d.ts b/typings/lib/parser/contents/Menu.d.ts new file mode 100644 index 000000000..64084acd1 --- /dev/null +++ b/typings/lib/parser/contents/Menu.d.ts @@ -0,0 +1,7 @@ +export = Menu; +declare class Menu { + constructor(item: any); + type: string; + top_level_buttons: any; + items: any; +} diff --git a/typings/lib/parser/contents/MenuNavigationItem.d.ts b/typings/lib/parser/contents/MenuNavigationItem.d.ts new file mode 100644 index 000000000..223a748f5 --- /dev/null +++ b/typings/lib/parser/contents/MenuNavigationItem.d.ts @@ -0,0 +1,9 @@ +export = MenuNavigationItem; +declare class MenuNavigationItem { + constructor(item: any); + type: string; + endpoint: NavigationEndpoint; + text: Text; +} +import NavigationEndpoint = require("./NavigationEndpoint"); +import Text = require("./Text"); diff --git a/typings/lib/parser/contents/MenuServiceItem.d.ts b/typings/lib/parser/contents/MenuServiceItem.d.ts new file mode 100644 index 000000000..09028d2a8 --- /dev/null +++ b/typings/lib/parser/contents/MenuServiceItem.d.ts @@ -0,0 +1,9 @@ +export = MenuServiceItem; +declare class MenuServiceItem { + constructor(item: any); + type: string; + endpoint: NavigationEndpoint; + text: Text; +} +import NavigationEndpoint = require("./NavigationEndpoint"); +import Text = require("./Text"); diff --git a/typings/lib/parser/contents/MetadataBadge.d.ts b/typings/lib/parser/contents/MetadataBadge.d.ts new file mode 100644 index 000000000..6d1d72d7a --- /dev/null +++ b/typings/lib/parser/contents/MetadataBadge.d.ts @@ -0,0 +1,8 @@ +export = MetadataBadge; +declare class MetadataBadge { + constructor(item: any); + type: string; + style: any; + label: any; + non_abbreviated_label: any; +} diff --git a/typings/lib/parser/contents/MetadataRow.d.ts b/typings/lib/parser/contents/MetadataRow.d.ts new file mode 100644 index 000000000..a31aca911 --- /dev/null +++ b/typings/lib/parser/contents/MetadataRow.d.ts @@ -0,0 +1,8 @@ +export = MetadataRow; +declare class MetadataRow { + constructor(item: any); + type: string; + contents: Text; + title: Text; +} +import Text = require("./Text"); diff --git a/typings/lib/parser/contents/MetadataRowContainer.d.ts b/typings/lib/parser/contents/MetadataRowContainer.d.ts new file mode 100644 index 000000000..a174caea3 --- /dev/null +++ b/typings/lib/parser/contents/MetadataRowContainer.d.ts @@ -0,0 +1,6 @@ +export = MetadataRowContainer; +declare class MetadataRowContainer { + constructor(item: any); + type: string; + rows: any; +} diff --git a/typings/lib/parser/contents/MetadataRowHeader.d.ts b/typings/lib/parser/contents/MetadataRowHeader.d.ts new file mode 100644 index 000000000..7e665e110 --- /dev/null +++ b/typings/lib/parser/contents/MetadataRowHeader.d.ts @@ -0,0 +1,7 @@ +export = MetadataRowHeader; +declare class MetadataRowHeader { + constructor(item: any); + type: string; + text: Text; +} +import Text = require("./Text"); diff --git a/typings/lib/parser/contents/Mix.d.ts b/typings/lib/parser/contents/Mix.d.ts new file mode 100644 index 000000000..2a1c96536 --- /dev/null +++ b/typings/lib/parser/contents/Mix.d.ts @@ -0,0 +1,6 @@ +export = Mix; +declare class Mix extends Playlist { + thumbnail: Thumbnail[]; +} +import Playlist = require("./Playlist"); +import Thumbnail = require("./Thumbnail"); diff --git a/typings/lib/parser/contents/MovingThumbnail.d.ts b/typings/lib/parser/contents/MovingThumbnail.d.ts new file mode 100644 index 000000000..61b32dd6d --- /dev/null +++ b/typings/lib/parser/contents/MovingThumbnail.d.ts @@ -0,0 +1,7 @@ +export = MovingThumbnail; +declare class MovingThumbnail { + constructor(item: any); + type: string; + thumbnails: Thumbnail[]; +} +import Thumbnail = require("./Thumbnail"); diff --git a/typings/lib/parser/contents/NavigatableText.d.ts b/typings/lib/parser/contents/NavigatableText.d.ts new file mode 100644 index 000000000..e957b7ebd --- /dev/null +++ b/typings/lib/parser/contents/NavigatableText.d.ts @@ -0,0 +1,9 @@ +export = NavigatableText; +declare class NavigatableText extends Text { + constructor(node: any); + endpoint: NavigationEndpoint; + toString(): string; + toJSON(): NavigatableText; +} +import Text = require("./Text"); +import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/NavigationEndpoint.d.ts b/typings/lib/parser/contents/NavigationEndpoint.d.ts new file mode 100644 index 000000000..ecd54edc6 --- /dev/null +++ b/typings/lib/parser/contents/NavigationEndpoint.d.ts @@ -0,0 +1,50 @@ +export = NavigationEndpoint; +declare class NavigationEndpoint { + constructor(item: any); + type: string; + metadata: { + api_url: any; + url: any; + send_post: any; + page_type: any; + }; + browse: { + browseId: any; + params: any; + base_url: any; + }; + watchVideo: { + video_id: any; + playlist_id: any; + index: any; + params: any; + }; + search: { + query: any; + params: any; + }; + watchPlaylist: any; + watchReel: { + video_id: any; + player_params: any; + params: any; + sequence_provider: any; + sequence_params: any; + }; + url: { + url: URL; + target: any; + nofollow: any; + }; + continuation: { + request: any; + token: any; + trigger: any; + }; + is_reveal_business_emal: boolean; + modal: { + title: any; + button: any; + content: any; + }; +} diff --git a/typings/lib/parser/contents/Playlist.d.ts b/typings/lib/parser/contents/Playlist.d.ts new file mode 100644 index 000000000..f4f35e956 --- /dev/null +++ b/typings/lib/parser/contents/Playlist.d.ts @@ -0,0 +1,16 @@ +export = Playlist; +declare class Playlist { + constructor(item: any); + type: string; + id: any; + title: Text; + author: Author; + thumbnails: any; + videos: number; + first_videos: any; + endpoint: NavigationEndpoint; + view_playlist: Text; +} +import Text = require("./Text"); +import Author = require("./Author"); +import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/PlaylistPanelVideo.d.ts b/typings/lib/parser/contents/PlaylistPanelVideo.d.ts new file mode 100644 index 000000000..338b677dc --- /dev/null +++ b/typings/lib/parser/contents/PlaylistPanelVideo.d.ts @@ -0,0 +1,17 @@ +export = PlaylistPanelVideo; +declare class PlaylistPanelVideo { + constructor(item: any); + type: string; + index: any; + selected: any; + duration: Text; + author: Author; + endpoint: NavigationEndpoint; + thumbnails: Thumbnail[]; + title: Text; + id: any; +} +import Text = require("./Text"); +import Author = require("./Author"); +import NavigationEndpoint = require("./NavigationEndpoint"); +import Thumbnail = require("./Thumbnail"); diff --git a/typings/lib/parser/contents/PlaylistVideo.d.ts b/typings/lib/parser/contents/PlaylistVideo.d.ts new file mode 100644 index 000000000..3cd42964d --- /dev/null +++ b/typings/lib/parser/contents/PlaylistVideo.d.ts @@ -0,0 +1,17 @@ +export = PlaylistVideo; +declare class PlaylistVideo { + constructor(item: any); + type: string; + index: any; + is_playable: any; + duration: Text; + endpoint: NavigationEndpoint; + author: NavigatableText; + thumbnails: Thumbnail[]; + title: Text; + id: any; +} +import Text = require("./Text"); +import NavigationEndpoint = require("./NavigationEndpoint"); +import NavigatableText = require("./NavigatableText"); +import Thumbnail = require("./Thumbnail"); diff --git a/typings/lib/parser/contents/PlaylistVideoList.d.ts b/typings/lib/parser/contents/PlaylistVideoList.d.ts new file mode 100644 index 000000000..5adbed9b9 --- /dev/null +++ b/typings/lib/parser/contents/PlaylistVideoList.d.ts @@ -0,0 +1,9 @@ +export = PlaylistVideoList; +declare class PlaylistVideoList { + constructor(item: any); + type: string; + is_editable: any; + can_reorder: any; + id: any; + contents: any; +} diff --git a/typings/lib/parser/contents/ReelItem.d.ts b/typings/lib/parser/contents/ReelItem.d.ts new file mode 100644 index 000000000..1f22cf71f --- /dev/null +++ b/typings/lib/parser/contents/ReelItem.d.ts @@ -0,0 +1,13 @@ +export = ReelItem; +declare class ReelItem { + constructor(item: any); + type: string; + id: any; + title: Text; + thumbnails: Thumbnail[]; + views: Text; + endpoint: NavigationEndpoint; +} +import Text = require("./Text"); +import Thumbnail = require("./Thumbnail"); +import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/ReelShelf.d.ts b/typings/lib/parser/contents/ReelShelf.d.ts new file mode 100644 index 000000000..2ca786058 --- /dev/null +++ b/typings/lib/parser/contents/ReelShelf.d.ts @@ -0,0 +1,10 @@ +export = ReelShelf; +declare class ReelShelf { + constructor(item: any); + type: string; + title: Text; + items: any; + endpoint: NavigationEndpoint; +} +import Text = require("./Text"); +import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/RichGrid.d.ts b/typings/lib/parser/contents/RichGrid.d.ts new file mode 100644 index 000000000..1e1f2351f --- /dev/null +++ b/typings/lib/parser/contents/RichGrid.d.ts @@ -0,0 +1,7 @@ +export = RichGrid; +declare class RichGrid { + constructor(item: any); + type: string; + header: any; + contents: any; +} diff --git a/typings/lib/parser/contents/RichShelf.d.ts b/typings/lib/parser/contents/RichShelf.d.ts new file mode 100644 index 000000000..a6e330ad9 --- /dev/null +++ b/typings/lib/parser/contents/RichShelf.d.ts @@ -0,0 +1,10 @@ +export = RichShelf; +declare class RichShelf { + constructor(item: any); + type: string; + title: Text; + contents: any; + endpoint: NavigationEndpoint; +} +import Text = require("./Text"); +import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/SearchRefinementCard.d.ts b/typings/lib/parser/contents/SearchRefinementCard.d.ts new file mode 100644 index 000000000..138b0b202 --- /dev/null +++ b/typings/lib/parser/contents/SearchRefinementCard.d.ts @@ -0,0 +1,12 @@ +export = SearchRefinementCard; +declare class SearchRefinementCard { + constructor(item: any); + type: string; + thumbnail: Thumbnail[]; + endpoint: NavigationEndpoint; + query: Text; + style: any; +} +import Thumbnail = require("./Thumbnail"); +import NavigationEndpoint = require("./NavigationEndpoint"); +import Text = require("./Text"); diff --git a/typings/lib/parser/contents/Shelf.d.ts b/typings/lib/parser/contents/Shelf.d.ts new file mode 100644 index 000000000..13dd1ded5 --- /dev/null +++ b/typings/lib/parser/contents/Shelf.d.ts @@ -0,0 +1,15 @@ +export = Shelf; +declare class Shelf { + constructor(item: any); + type: string; + title: Text; + content: any; + endpoint: NavigationEndpoint; + button: { + text: Text; + endpoint: NavigationEndpoint; + icon: any; + }; +} +import Text = require("./Text"); +import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/Tab.d.ts b/typings/lib/parser/contents/Tab.d.ts new file mode 100644 index 000000000..3ef3720e7 --- /dev/null +++ b/typings/lib/parser/contents/Tab.d.ts @@ -0,0 +1,10 @@ +export = Tab; +declare class Tab { + constructor(item: any); + type: string; + title: any; + endpoint: NavigationEndpoint; + selected: any; + content: any; +} +import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/Text.d.ts b/typings/lib/parser/contents/Text.d.ts new file mode 100644 index 000000000..0d1faa874 --- /dev/null +++ b/typings/lib/parser/contents/Text.d.ts @@ -0,0 +1,12 @@ +export = Text; +declare class Text { + constructor(txt: any, def?: any); + type: string; + text: any; + runs: any; + toString(): any; + toJSON(): { + text: any; + runs: any; + }; +} diff --git a/typings/lib/parser/contents/TextRun.d.ts b/typings/lib/parser/contents/TextRun.d.ts new file mode 100644 index 000000000..94b207a85 --- /dev/null +++ b/typings/lib/parser/contents/TextRun.d.ts @@ -0,0 +1,8 @@ +export = TextRun; +declare class TextRun { + constructor(node: any); + type: string; + text: any; + endpoint: NavigationEndpoint; +} +import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/Thumbnail.d.ts b/typings/lib/parser/contents/Thumbnail.d.ts new file mode 100644 index 000000000..6635eb78f --- /dev/null +++ b/typings/lib/parser/contents/Thumbnail.d.ts @@ -0,0 +1,26 @@ +export = Thumbnail; +declare class Thumbnail { + /** + * Get thumbnails from response object + * @param {*} response response object + * @returns {Thumbnail[]} sorted array of thumbnails + */ + static fromResponse({ thumbnails }: any): Thumbnail[]; + constructor({ url, width, height }: { + url: any; + width: any; + height: any; + }); + /** + * @type {string} + */ + url: string; + /** + * @type {number} + */ + width: number; + /** + * @type {number} + */ + height: number; +} diff --git a/typings/lib/parser/contents/ToggleButton.d.ts b/typings/lib/parser/contents/ToggleButton.d.ts new file mode 100644 index 000000000..ab57186a7 --- /dev/null +++ b/typings/lib/parser/contents/ToggleButton.d.ts @@ -0,0 +1,19 @@ +export = ToggleButton; +declare class ToggleButton { + constructor(item: any); + type: string; + is_toggled: any; + is_disabled: any; + default_service_endpoint: NavigationEndpoint; + toggled_service_endpoint: NavigationEndpoint; + default_navigation_endpoint: NavigationEndpoint; + default_tooltip: any; + toggled_tooltip: any; + default_text: Text; + toggled_text: Text; + get endpoint(): NavigationEndpoint; + get tooltip(): any; + get text(): Text; +} +import NavigationEndpoint = require("./NavigationEndpoint"); +import Text = require("./Text"); diff --git a/typings/lib/parser/contents/TwoColumnBrowseResults.d.ts b/typings/lib/parser/contents/TwoColumnBrowseResults.d.ts new file mode 100644 index 000000000..c5a0409ad --- /dev/null +++ b/typings/lib/parser/contents/TwoColumnBrowseResults.d.ts @@ -0,0 +1,6 @@ +export = TwoColumnBrowseResults; +declare class TwoColumnBrowseResults { + constructor(item: any); + type: string; + tabs: any; +} diff --git a/typings/lib/parser/contents/TwoColumnSearchResults.d.ts b/typings/lib/parser/contents/TwoColumnSearchResults.d.ts new file mode 100644 index 000000000..9906bd4b1 --- /dev/null +++ b/typings/lib/parser/contents/TwoColumnSearchResults.d.ts @@ -0,0 +1,7 @@ +export = TwoColumnSearchResults; +declare class TwoColumnSearchResults { + constructor(items: any); + type: string; + primary: any; + secondary: any; +} diff --git a/typings/lib/parser/contents/TwoColumnWatchNextResults.d.ts b/typings/lib/parser/contents/TwoColumnWatchNextResults.d.ts new file mode 100644 index 000000000..9b7d60983 --- /dev/null +++ b/typings/lib/parser/contents/TwoColumnWatchNextResults.d.ts @@ -0,0 +1,21 @@ +export = TwoColumnWatchNextResult; +declare class TwoColumnWatchNextResult { + constructor(item: any); + type: string; + primary: any; + secondary: any; + playlist: { + current_index: any; + endpoint: NavigationEndpoint; + is_course: any; + is_infinite: any; + author: Author; + save: any; + title: NavigatableText; + videos: any; + contents: any; + }; +} +import NavigationEndpoint = require("./NavigationEndpoint"); +import Author = require("./Author"); +import NavigatableText = require("./NavigatableText"); diff --git a/typings/lib/parser/contents/UniversalWatchCard.d.ts b/typings/lib/parser/contents/UniversalWatchCard.d.ts new file mode 100644 index 000000000..6b630283c --- /dev/null +++ b/typings/lib/parser/contents/UniversalWatchCard.d.ts @@ -0,0 +1,8 @@ +export = UniversalWatchCard; +declare class UniversalWatchCard { + constructor(items: any); + type: string; + header: any; + hero: any; + sections: any; +} diff --git a/typings/lib/parser/contents/VerticalList.d.ts b/typings/lib/parser/contents/VerticalList.d.ts new file mode 100644 index 000000000..1501d47e7 --- /dev/null +++ b/typings/lib/parser/contents/VerticalList.d.ts @@ -0,0 +1,7 @@ +export = VerticalList; +declare class VerticalList { + constructor(item: any); + type: string; + collapsed_item_count: any; + items: any; +} diff --git a/typings/lib/parser/contents/Video.d.ts b/typings/lib/parser/contents/Video.d.ts new file mode 100644 index 000000000..40f2a3d66 --- /dev/null +++ b/typings/lib/parser/contents/Video.d.ts @@ -0,0 +1,88 @@ +export = Video; +declare class Video { + constructor(item: any); + type: string; + author: Author; + /** + * @type {import('./MetadataBadge')[]} + */ + badges: import('./MetadataBadge')[]; + /** + * @type {Thumbnail[]} + */ + thumbnails: Thumbnail[]; + /** + * @type {import('./MovingThumbnail') | undefined} + */ + rich_thumbnail: import('./MovingThumbnail') | undefined; + /** + * @type {Date | undefined} + */ + upcoming: Date | undefined; + /** + * @type {string} + */ + id: string; + /** + * @type {Text} + */ + title: Text; + /** + * @type {string} + */ + duration: string; + /** + * @type {Text} + */ + published_at: Text; + /** + * @type {Text} + */ + views: Text; + /** + * @type {{ + * text: Text, + * hoverText: Text, + * }[]} + */ + snippets: { + text: Text; + hoverText: Text; + }[]; + /** + * @type {Text} + */ + description_snippet: Text; + /** + * @type {Text} + */ + short_view_count: Text; + /** + * @type {NavigationEndpoint} + */ + endpoint: NavigationEndpoint; + /** + * @returns {string} + */ + get description(): string; + /** + * @type {boolean} + */ + get is_live(): boolean; + /** + * @type {boolean} + */ + get is_upcoming(): boolean; + /** + * @type {boolean} + */ + get has_captions(): boolean; + /** + * @type {Thumbnail | undefined} + */ + get best_thumbnail(): Thumbnail; +} +import Author = require("./Author"); +import Thumbnail = require("./Thumbnail"); +import Text = require("./Text"); +import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/VideoOwner.d.ts b/typings/lib/parser/contents/VideoOwner.d.ts new file mode 100644 index 000000000..400f4dcb7 --- /dev/null +++ b/typings/lib/parser/contents/VideoOwner.d.ts @@ -0,0 +1,5 @@ +export = VideoOwner; +declare class VideoOwner { + constructor(item: any); + type: string; +} diff --git a/typings/lib/parser/contents/VideoPrimaryInfo.d.ts b/typings/lib/parser/contents/VideoPrimaryInfo.d.ts new file mode 100644 index 000000000..d022643a5 --- /dev/null +++ b/typings/lib/parser/contents/VideoPrimaryInfo.d.ts @@ -0,0 +1,10 @@ +export = VideoPrimaryInfo; +declare class VideoPrimaryInfo { + constructor(item: any); + type: string; + title: Text; + published_at: Date; + actions: any; + views: Text; +} +import Text = require("./Text"); diff --git a/typings/lib/parser/contents/VideoSecondaryInfo.d.ts b/typings/lib/parser/contents/VideoSecondaryInfo.d.ts new file mode 100644 index 000000000..a942a33a2 --- /dev/null +++ b/typings/lib/parser/contents/VideoSecondaryInfo.d.ts @@ -0,0 +1,10 @@ +export = VideoSecondaryInfo; +declare class VideoSecondaryInfo { + constructor(item: any); + type: string; + owner: any; + description: Text; + metadata: any; + description_collapsed_lines: any; +} +import Text = require("./Text"); diff --git a/typings/lib/parser/contents/WatchCardCompactVideo.d.ts b/typings/lib/parser/contents/WatchCardCompactVideo.d.ts new file mode 100644 index 000000000..0d92c4821 --- /dev/null +++ b/typings/lib/parser/contents/WatchCardCompactVideo.d.ts @@ -0,0 +1,14 @@ +export = WatchCardCompactVideo; +declare class WatchCardCompactVideo { + constructor(item: any); + type: string; + title: Text; + views: any; + published_at: any; + endpoint: NavigationEndpoint; + duration: Text; + byline: NavigatableText; +} +import Text = require("./Text"); +import NavigationEndpoint = require("./NavigationEndpoint"); +import NavigatableText = require("./NavigatableText"); diff --git a/typings/lib/parser/contents/WatchCardHeroVideo.d.ts b/typings/lib/parser/contents/WatchCardHeroVideo.d.ts new file mode 100644 index 000000000..211ed8d73 --- /dev/null +++ b/typings/lib/parser/contents/WatchCardHeroVideo.d.ts @@ -0,0 +1,8 @@ +export = WatchCardHeroVideo; +declare class WatchCardHeroVideo { + constructor(item: any); + type: string; + endpoint: NavigationEndpoint; + collage: any; +} +import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/WatchCardRichHeader.d.ts b/typings/lib/parser/contents/WatchCardRichHeader.d.ts new file mode 100644 index 000000000..361b87cea --- /dev/null +++ b/typings/lib/parser/contents/WatchCardRichHeader.d.ts @@ -0,0 +1,10 @@ +export = WatchCardRichHeader; +declare class WatchCardRichHeader { + constructor(item: any); + type: string; + title: Text; + subtitle: Text; + author: Author; +} +import Text = require("./Text"); +import Author = require("./Author"); diff --git a/typings/lib/parser/contents/WatchCardSectionSequence.d.ts b/typings/lib/parser/contents/WatchCardSectionSequence.d.ts new file mode 100644 index 000000000..353d574de --- /dev/null +++ b/typings/lib/parser/contents/WatchCardSectionSequence.d.ts @@ -0,0 +1,6 @@ +export = WatchCardSectionSequence; +declare class WatchCardSectionSequence { + constructor(item: any); + type: string; + lists: any; +} diff --git a/typings/lib/parser/contents/index.d.ts b/typings/lib/parser/contents/index.d.ts new file mode 100644 index 000000000..0762a3370 --- /dev/null +++ b/typings/lib/parser/contents/index.d.ts @@ -0,0 +1,8 @@ +export = ResultsParser; +declare class ResultsParser { + static parseResponse(data: any): { + contents: any; + }; + static parse(contents: any): any; + static parseItem(item: any): any; +} diff --git a/typings/lib/parser/index.d.ts b/typings/lib/parser/index.d.ts new file mode 100644 index 000000000..6fdbae582 --- /dev/null +++ b/typings/lib/parser/index.d.ts @@ -0,0 +1,9 @@ +export = Parser; +declare class Parser { + constructor(session: any, data: any, args?: {}); + data: any; + session: any; + args: {}; + parse(): any; + #private; +} diff --git a/typings/lib/parser/simplify.d.ts b/typings/lib/parser/simplify.d.ts new file mode 100644 index 000000000..f0b9ef72c --- /dev/null +++ b/typings/lib/parser/simplify.d.ts @@ -0,0 +1,9 @@ +export = Simplify; +declare class Simplify { + static matching(match: any): Simplify; + static matchingAny(match: any): Simplify; + constructor(schema: any); + match(data: any): any[]; + runOn(data: any): any; + #private; +} diff --git a/typings/lib/parser/youtube/index.d.ts b/typings/lib/parser/youtube/index.d.ts new file mode 100644 index 000000000..3a9bdf362 --- /dev/null +++ b/typings/lib/parser/youtube/index.d.ts @@ -0,0 +1,11 @@ +import VideoResultItem = require("./search/VideoResultItem"); +import SearchSuggestionItem = require("./search/SearchSuggestionItem"); +import PlaylistItem = require("./others/PlaylistItem"); +import NotificationItem = require("./others/NotificationItem"); +import VideoItem = require("./others/VideoItem"); +import GridVideoItem = require("./others/GridVideoItem"); +import GridPlaylistItem = require("./others/GridPlaylistItem"); +import ChannelMetadata = require("./others/ChannelMetadata"); +import ShelfRenderer = require("./others/ShelfRenderer"); +import CommentThread = require("./others/CommentThread"); +export { VideoResultItem, SearchSuggestionItem, PlaylistItem, NotificationItem, VideoItem, GridVideoItem, GridPlaylistItem, ChannelMetadata, ShelfRenderer, CommentThread }; diff --git a/typings/lib/parser/youtube/others/ChannelMetadata.d.ts b/typings/lib/parser/youtube/others/ChannelMetadata.d.ts new file mode 100644 index 000000000..0057c7aad --- /dev/null +++ b/typings/lib/parser/youtube/others/ChannelMetadata.d.ts @@ -0,0 +1,15 @@ +export = ChannelMetadata; +declare class ChannelMetadata { + static parse(data: any): { + title: any; + description: any; + metadata: { + url: any; + rss_urls: any; + vanity_channel_url: any; + external_id: any; + is_family_safe: any; + keywords: any; + }; + }; +} diff --git a/typings/lib/parser/youtube/others/CommentThread.d.ts b/typings/lib/parser/youtube/others/CommentThread.d.ts new file mode 100644 index 000000000..971d2ee44 --- /dev/null +++ b/typings/lib/parser/youtube/others/CommentThread.d.ts @@ -0,0 +1,23 @@ +export = CommentThread; +declare class CommentThread { + static parseItem(item: any): { + text: any; + author: { + name: any; + thumbnails: any; + channel_id: any; + channel_url: string; + }; + metadata: { + published: any; + is_reply: boolean; + is_liked: any; + is_disliked: any; + is_pinned: boolean; + is_channel_owner: any; + like_count: number; + reply_count: any; + id: any; + }; + }; +} diff --git a/typings/lib/parser/youtube/others/GridPlaylistItem.d.ts b/typings/lib/parser/youtube/others/GridPlaylistItem.d.ts new file mode 100644 index 000000000..b990a83c0 --- /dev/null +++ b/typings/lib/parser/youtube/others/GridPlaylistItem.d.ts @@ -0,0 +1,12 @@ +export = GridPlaylistItem; +declare class GridPlaylistItem { + static parse(data: any): any; + static parseItem(item: any): { + id: any; + title: any; + metadata: { + thumbnail: any; + video_count: any; + }; + }; +} diff --git a/typings/lib/parser/youtube/others/GridVideoItem.d.ts b/typings/lib/parser/youtube/others/GridVideoItem.d.ts new file mode 100644 index 000000000..da609705f --- /dev/null +++ b/typings/lib/parser/youtube/others/GridVideoItem.d.ts @@ -0,0 +1,25 @@ +export = GridVideoItem; +declare class GridVideoItem { + static parse(data: any): any; + static parseItem(item: any): { + id: any; + title: any; + channel: { + id: any; + name: any; + url: string; + }; + metadata: { + view_count: any; + short_view_count_text: { + simple_text: any; + accessibility_label: any; + }; + thumbnail: any; + moving_thumbnail: any; + published: any; + badges: any; + owner_badges: any; + }; + }; +} diff --git a/typings/lib/parser/youtube/others/NotificationItem.d.ts b/typings/lib/parser/youtube/others/NotificationItem.d.ts new file mode 100644 index 000000000..d0e779c69 --- /dev/null +++ b/typings/lib/parser/youtube/others/NotificationItem.d.ts @@ -0,0 +1,14 @@ +export = NotificationItem; +declare class NotificationItem { + static parse(data: any): any; + static parseItem(item: any): { + title: any; + sent_time: any; + channel_name: any; + channel_thumbnail: any; + video_thumbnail: any; + video_url: string; + read: any; + notification_id: any; + }; +} diff --git a/typings/lib/parser/youtube/others/PlaylistItem.d.ts b/typings/lib/parser/youtube/others/PlaylistItem.d.ts new file mode 100644 index 000000000..54e2e4500 --- /dev/null +++ b/typings/lib/parser/youtube/others/PlaylistItem.d.ts @@ -0,0 +1,15 @@ +export = PlaylistItem; +declare class PlaylistItem { + static parse(data: any): any; + static parseItem(item: any): { + id: any; + title: any; + author: any; + duration: { + seconds: number; + simple_text: any; + accessibility_label: any; + }; + thumbnails: any; + }; +} diff --git a/typings/lib/parser/youtube/others/ShelfRenderer.d.ts b/typings/lib/parser/youtube/others/ShelfRenderer.d.ts new file mode 100644 index 000000000..cf080beda --- /dev/null +++ b/typings/lib/parser/youtube/others/ShelfRenderer.d.ts @@ -0,0 +1,9 @@ +export = ShelfRenderer; +declare class ShelfRenderer { + static parse(data: any): { + title: any; + videos: any; + }; + static getTitle(data: any): any; + static parseItems(data: any): any; +} diff --git a/typings/lib/parser/youtube/others/VideoItem.d.ts b/typings/lib/parser/youtube/others/VideoItem.d.ts new file mode 100644 index 000000000..38e42c751 --- /dev/null +++ b/typings/lib/parser/youtube/others/VideoItem.d.ts @@ -0,0 +1,31 @@ +export = VideoItem; +declare class VideoItem { + static parse(data: any): any; + static parseItem(item: any): { + id: any; + title: any; + description: any; + channel: { + id: any; + name: any; + url: string; + }; + metadata: { + view_count: any; + short_view_count_text: { + simple_text: any; + accessibility_label: any; + }; + thumbnail: any; + moving_thumbnail: any; + published: any; + duration: { + seconds: number; + simple_text: any; + accessibility_label: any; + }; + badges: any; + owner_badges: any; + }; + }; +} diff --git a/typings/lib/parser/youtube/search/SearchSuggestionItem.d.ts b/typings/lib/parser/youtube/search/SearchSuggestionItem.d.ts new file mode 100644 index 000000000..d382ab081 --- /dev/null +++ b/typings/lib/parser/youtube/search/SearchSuggestionItem.d.ts @@ -0,0 +1,4 @@ +export = SearchSuggestionItem; +declare class SearchSuggestionItem { + static parse(data: any, bold_text: any): any; +} diff --git a/typings/lib/parser/youtube/search/VideoResultItem.d.ts b/typings/lib/parser/youtube/search/VideoResultItem.d.ts new file mode 100644 index 000000000..65e06c68c --- /dev/null +++ b/typings/lib/parser/youtube/search/VideoResultItem.d.ts @@ -0,0 +1,31 @@ +export = VideoResultItem; +declare class VideoResultItem { + static parse(data: any): any; + static parseItem(item: any): { + id: any; + url: string; + title: any; + description: any; + channel: { + id: any; + name: any; + url: string; + }; + metadata: { + view_count: any; + short_view_count_text: { + simple_text: any; + accessibility_label: any; + }; + thumbnails: any; + duration: { + seconds: number; + simple_text: any; + accessibility_label: any; + }; + published: any; + badges: any; + owner_badges: any; + }; + }; +} diff --git a/typings/lib/parser/ytmusic/index.d.ts b/typings/lib/parser/ytmusic/index.d.ts new file mode 100644 index 000000000..f22d9dbf1 --- /dev/null +++ b/typings/lib/parser/ytmusic/index.d.ts @@ -0,0 +1,9 @@ +import SongResultItem = require("./search/SongResultItem"); +import VideoResultItem = require("./search/VideoResultItem"); +import AlbumResultItem = require("./search/AlbumResultItem"); +import ArtistResultItem = require("./search/ArtistResultItem"); +import PlaylistResultItem = require("./search/PlaylistResultItem"); +import MusicSearchSuggestionItem = require("./search/MusicSearchSuggestionItem"); +import TopResultItem = require("./search/TopResultItem"); +import PlaylistItem = require("./others/PlaylistItem"); +export { SongResultItem, VideoResultItem, AlbumResultItem, ArtistResultItem, PlaylistResultItem, MusicSearchSuggestionItem, TopResultItem, PlaylistItem }; diff --git a/typings/lib/parser/ytmusic/others/PlaylistItem.d.ts b/typings/lib/parser/ytmusic/others/PlaylistItem.d.ts new file mode 100644 index 000000000..6e7bdbf1f --- /dev/null +++ b/typings/lib/parser/ytmusic/others/PlaylistItem.d.ts @@ -0,0 +1,14 @@ +export = PlaylistItem; +declare class PlaylistItem { + static parse(data: any): any; + static parseItem(item: any): { + id: any; + title: any; + author: any; + duration: { + seconds: number; + simple_text: any; + }; + thumbnails: any; + }; +} diff --git a/typings/lib/parser/ytmusic/search/AlbumResultItem.d.ts b/typings/lib/parser/ytmusic/search/AlbumResultItem.d.ts new file mode 100644 index 000000000..13cf51825 --- /dev/null +++ b/typings/lib/parser/ytmusic/search/AlbumResultItem.d.ts @@ -0,0 +1,11 @@ +export = AlbumResultItem; +declare class AlbumResultItem { + static parse(data: any): any; + static parseItem(item: any): { + id: any; + title: any; + author: any; + year: any; + thumbnails: any; + }; +} diff --git a/typings/lib/parser/ytmusic/search/ArtistResultItem.d.ts b/typings/lib/parser/ytmusic/search/ArtistResultItem.d.ts new file mode 100644 index 000000000..01b10e58f --- /dev/null +++ b/typings/lib/parser/ytmusic/search/ArtistResultItem.d.ts @@ -0,0 +1,10 @@ +export = ArtistResultItem; +declare class ArtistResultItem { + static parse(data: any): any; + static parseItem(item: any): { + id: any; + name: any; + subscribers: any; + thumbnails: any; + }; +} diff --git a/typings/lib/parser/ytmusic/search/MusicSearchSuggestionItem.d.ts b/typings/lib/parser/ytmusic/search/MusicSearchSuggestionItem.d.ts new file mode 100644 index 000000000..2891d3773 --- /dev/null +++ b/typings/lib/parser/ytmusic/search/MusicSearchSuggestionItem.d.ts @@ -0,0 +1,8 @@ +export = MusicSearchSuggestionItem; +declare class MusicSearchSuggestionItem { + static parse(data: any): any; + static parseItem(item: any): { + text: any; + bold_text: any; + }; +} diff --git a/typings/lib/parser/ytmusic/search/PlaylistResultItem.d.ts b/typings/lib/parser/ytmusic/search/PlaylistResultItem.d.ts new file mode 100644 index 000000000..0a4d4e15f --- /dev/null +++ b/typings/lib/parser/ytmusic/search/PlaylistResultItem.d.ts @@ -0,0 +1,11 @@ +export = PlaylistResultItem; +declare class PlaylistResultItem { + static parse(data: any): any; + static parseItem(item: any): { + id: any; + title: any; + author: any; + channel_id: any; + total_items: number; + }; +} diff --git a/typings/lib/parser/ytmusic/search/SongResultItem.d.ts b/typings/lib/parser/ytmusic/search/SongResultItem.d.ts new file mode 100644 index 000000000..c36f509a0 --- /dev/null +++ b/typings/lib/parser/ytmusic/search/SongResultItem.d.ts @@ -0,0 +1,12 @@ +export = SongResultItem; +declare class SongResultItem { + static parse(data: any): any; + static parseItem(item: any): { + id: any; + title: any; + artist: any; + album: any; + duration: any; + thumbnails: any; + }; +} diff --git a/typings/lib/parser/ytmusic/search/TopResultItem.d.ts b/typings/lib/parser/ytmusic/search/TopResultItem.d.ts new file mode 100644 index 000000000..10d187acf --- /dev/null +++ b/typings/lib/parser/ytmusic/search/TopResultItem.d.ts @@ -0,0 +1,4 @@ +export = TopResultItem; +declare class TopResultItem { + static parse(data: any): any; +} diff --git a/typings/lib/parser/ytmusic/search/VideoResultItem.d.ts b/typings/lib/parser/ytmusic/search/VideoResultItem.d.ts new file mode 100644 index 000000000..5ace4608c --- /dev/null +++ b/typings/lib/parser/ytmusic/search/VideoResultItem.d.ts @@ -0,0 +1,12 @@ +export = VideoResultItem; +declare class VideoResultItem { + static parse(data: any): any; + static parseItem(item: any): { + id: any; + title: any; + author: any; + views: any; + duration: any; + thumbnails: any; + }; +} diff --git a/typings/lib/proto/index.d.ts b/typings/lib/proto/index.d.ts new file mode 100644 index 000000000..b195e0012 --- /dev/null +++ b/typings/lib/proto/index.d.ts @@ -0,0 +1,11 @@ +export = Proto; +declare class Proto { + static encodeSearchFilter(period: any, duration: any, order: any): string; + static encodeMessageParams(channel_id: any, video_id: any): any; + static encodeCommentsSectionParams(video_id: any, options?: {}): string; + static encodeCommentRepliesParams(video_id: any, comment_id: any): string; + static encodeCommentParams(video_id: any): string; + static encodeCommentReplyParams(comment_id: any, video_id: any): string; + static encodeCommentActionParams(type: any, comment_id: any, video_id: any): string; + static encodeNotificationPref(channel_id: any, index: any): string; +} diff --git a/typings/lib/proto/messages.d.ts b/typings/lib/proto/messages.d.ts new file mode 100644 index 000000000..5145c4974 --- /dev/null +++ b/typings/lib/proto/messages.d.ts @@ -0,0 +1,66 @@ +export namespace NotificationPreferences { + const buffer: boolean; + const encodingLength: any; + const encode: any; + const decode: any; +} +export namespace LiveMessageParams { + const buffer_1: boolean; + export { buffer_1 as buffer }; + const encodingLength_1: any; + export { encodingLength_1 as encodingLength }; + const encode_1: any; + export { encode_1 as encode }; + const decode_1: any; + export { decode_1 as decode }; +} +export namespace GetCommentsSectionParams { + const buffer_2: boolean; + export { buffer_2 as buffer }; + const encodingLength_2: any; + export { encodingLength_2 as encodingLength }; + const encode_2: any; + export { encode_2 as encode }; + const decode_2: any; + export { decode_2 as decode }; +} +export namespace CreateCommentParams { + const buffer_3: boolean; + export { buffer_3 as buffer }; + const encodingLength_3: any; + export { encodingLength_3 as encodingLength }; + const encode_3: any; + export { encode_3 as encode }; + const decode_3: any; + export { decode_3 as decode }; +} +export namespace CreateCommentReplyParams { + const buffer_4: boolean; + export { buffer_4 as buffer }; + const encodingLength_4: any; + export { encodingLength_4 as encodingLength }; + const encode_4: any; + export { encode_4 as encode }; + const decode_4: any; + export { decode_4 as decode }; +} +export namespace PeformCommentActionParams { + const buffer_5: boolean; + export { buffer_5 as buffer }; + const encodingLength_5: any; + export { encodingLength_5 as encodingLength }; + const encode_5: any; + export { encode_5 as encode }; + const decode_5: any; + export { decode_5 as decode }; +} +export namespace SearchFilter { + const buffer_6: boolean; + export { buffer_6 as buffer }; + const encodingLength_6: any; + export { encodingLength_6 as encodingLength }; + const encode_6: any; + export { encode_6 as encode }; + const decode_6: any; + export { decode_6 as decode }; +} diff --git a/typings/lib/utils/Constants.d.ts b/typings/lib/utils/Constants.d.ts new file mode 100644 index 000000000..0ba75b8ce --- /dev/null +++ b/typings/lib/utils/Constants.d.ts @@ -0,0 +1,101 @@ +export namespace URLS { + const YT_BASE: string; + const YT_BASE_API: string; + const YT_STUDIO_BASE_API: string; + const YT_SUGGESTIONS: string; + const YT_MUSIC: string; + const YT_MUSIC_BASE_API: string; +} +export namespace OAUTH { + const SCOPE: string; + const GRANT_TYPE: string; + const MODEL_NAME: string; + namespace HEADERS { + const headers: { + accept: string; + origin: string; + 'user-agent': string; + 'content-type': string; + referer: string; + 'accept-language': string; + }; + } + namespace REGEX { + const AUTH_SCRIPT: RegExp; + const CLIENT_IDENTITY: RegExp; + } +} +export function DEFAULT_HEADERS(config: any): { + headers: { + Cookie: any; + 'user-agent': any; + Referer: string; + Accept: string; + 'Accept-Language': string; + 'Accept-Encoding': string; + }; +}; +export const STREAM_HEADERS: { + Accept: string; + 'User-Agent': any; + Connection: string; + Origin: string; + Referer: string; + DNT: string; +}; +export const INNERTUBE_HEADERS_BASE: { + accept: string; + 'content-type': string; +}; +export function VIDEO_INFO_REQBODY(id: any, sts: any, context: any): { + playbackContext: { + contentPlaybackContext: { + currentUrl: string; + vis: number; + splay: boolean; + autoCaptionsDefaultOn: boolean; + autonavState: string; + html5Preference: string; + signatureTimestamp: any; + referer: string; + lactMilliseconds: string; + }; + }; + context: any; + videoId: any; +}; +export const YTMUSIC_VERSION: string; +export const METADATA_KEYS: string[]; +export const BLACKLISTED_KEYS: string[]; +export namespace ACCOUNT_SETTINGS { + const SUBSCRIPTIONS: string; + const RECOMMENDED_VIDEOS: string; + const CHANNEL_ACTIVITY: string; + const COMMENT_REPLIES: string; + const USER_MENTION: string; + const SHARED_CONTENT: string; + const PLAYLISTS_PRIVACY: string; + const SUBSCRIPTIONS_PRIVACY: string; +} +export namespace BASE64_DIALECT { + const NORMAL: string[]; + const REVERSE: string[]; +} +export namespace NTOKEN_REGEX { + const CALLS: RegExp; + const PLACEHOLDERS: RegExp; +} +export const FUNCS_REGEX: RegExp; +export namespace FUNCS { + const PUSH: string; + const REVERSE_1: string; + const REVERSE_2: string; + const SPLICE: string; + const SWAP0_1: string; + const SWAP0_2: string; + const ROTATE_1: string; + const ROTATE_2: string; + const BASE64_DIA: string; + const TRANSLATE_1: string; + const TRANSLATE_2: string; +} diff --git a/typings/lib/utils/Request.d.ts b/typings/lib/utils/Request.d.ts new file mode 100644 index 000000000..97b7f0331 --- /dev/null +++ b/typings/lib/utils/Request.d.ts @@ -0,0 +1,7 @@ +export = Request; +declare class Request { + constructor(session: any); + session: any; + instance: any; + #private; +} diff --git a/typings/lib/utils/Utils.d.ts b/typings/lib/utils/Utils.d.ts new file mode 100644 index 000000000..43f8b6d91 --- /dev/null +++ b/typings/lib/utils/Utils.d.ts @@ -0,0 +1,70 @@ +export class UnavailableContentError extends InnertubeError { +} +export class ParsingError extends InnertubeError { +} +export class DownloadError extends InnertubeError { +} +export function InnertubeError(message: any, info: any): void; +export class InnertubeError { + constructor(message: any, info: any); + info: any; + stack: string; + constructor: typeof InnertubeError; +} +export class MissingParamError extends InnertubeError { +} +export class NoStreamingDataError extends InnertubeError { +} +/** + * Utility to help access deep properties of an object. + * + * @param {object} obj - The object. + * @param {string} key - Key of the property being accessed. + * @param {string} target - Anything that might be inside of the property. + * @param {number} depth - Maximum number of nested objects to flatten. + * @param {boolean} safe - If set to true arrays will be preserved. + */ +export function findNode(obj: object, key: string, target: string, depth: number, safe?: boolean): any; +/** + * Returns a random user agent. + * + * @param {string} type - mobile | desktop + * @returns {object} + */ +export function getRandomUserAgent(type: string): object; +/** + * Generates an authentication token from a cookies' sid. + * + * @param {string} sid - Sid extracted from cookies + * @returns {string} + */ +export function generateSidAuth(sid: string): string; +/** + * Gets a string between two delimiters. + * + * @param {string} data - The data. + * @param {string} start_string - Start string. + * @param {string} end_string - End string. + */ +export function getStringBetweenStrings(data: string, start_string: string, end_string: string): string; +/** + * Converts strings in camelCase to snake_case. + * + * @param {string} string The string in camelCase. + * @returns {string} + */ +export function camelToSnake(string: string): string; +/** + * Converts time (h:m:s) to seconds. + * + * @param {string} time + * @returns {number} seconds + */ +export function timeToSeconds(time: string): number; +/** + * Turns the ntoken transform data into a valid json array + * + * @param {string} data + * @returns {string} + */ +export function refineNTokenData(data: string): string; From 36c08cdd5c9186bd5cd34f7b2283f7b3582ce0c0 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sun, 1 May 2022 04:27:31 +0200 Subject: [PATCH 14/55] feat: browse continuations from navigationEndpoint --- lib/Innertube.js | 4 +- lib/core/HomeFeed.js | 76 ++++++++++++++++++----- lib/parser/contents/ContinuationItem.js | 21 +++++++ lib/parser/contents/NavigationEndpoint.js | 16 ++++- lib/parser/contents/index.js | 18 +++++- typings/lib/Innertube.d.ts | 13 +--- 6 files changed, 119 insertions(+), 29 deletions(-) diff --git a/lib/Innertube.js b/lib/Innertube.js index 109400a6b..4249b1f6a 100644 --- a/lib/Innertube.js +++ b/lib/Innertube.js @@ -545,13 +545,13 @@ class Innertube { /** * Retrieves YouTube's home feed (aka recommendations). - * @returns {Promise.<{ videos: [{ id: string; title: string; description: string; channel: string; metadata: object }] }>} + * @returns {Promise} */ async getHomeFeed() { const response = await Actions.browse(this, 'home_feed'); if (!response.success) throw new Utils.InnertubeError('Could not retrieve home feed', response); - return new HomeFeed(response.data); + return new HomeFeed(this, response.data); } /** diff --git a/lib/core/HomeFeed.js b/lib/core/HomeFeed.js index 8d95234f9..ca0a3197e 100644 --- a/lib/core/HomeFeed.js +++ b/lib/core/HomeFeed.js @@ -1,13 +1,25 @@ const ResultsParser = require("../parser/contents"); const Simplify = require("../parser/simplify"); +const { InnertubeError } = require("../utils/Utils"); const Utils = require("../utils/Utils"); class HomeFeed { #page; - #videos; - #video_elements; - constructor(data) { - this.#page = ResultsParser.parseResponse(data); + /** + * @type {import('../parser/contents/ContinuationItem')[]} + */ + #continuation; + /** + * @type {import('../parser/contents/ChipCloudChip')[]} + */ + #chips; + #session; + constructor(session, data) { + if (data.on_response_received_actions) + this.#page = data; + else + this.#page = ResultsParser.parseResponse(data); + this.#session = session; } /** @@ -22,13 +34,33 @@ class HomeFeed { * @returns {Array} */ getVideos() { - if (this.#video_elements) return this.#video_elements; - - this.#video_elements = Simplify.matching({ + return Simplify.matching({ type: Simplify.matching(/^Video$/), }).runOn(this.#page); + } - return this.#video_elements; + /** + * Get filters for the home feed + * @returns {import('../parser/contents/ChipCloudChip')[]} + */ + getFeedFilters() { + if (this.#chips) return this.#chips; + + const chipbars = Simplify.matching({ + type: Simplify.matching(/^FeedFilterChipBar$/), + }).runOn(this.#page); + + if (chipbars.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 (chipbars.length === 0) + throw new InnertubeError('There are no feed filter chipbars'); + const chipbar = chipbars[0]; + + this.#chips = Simplify.matching({ + type: Simplify.matching(/^ChipCloudChip$/), + }).runOn(chipbar); + + return this.getFeedFilters(); } /** @@ -36,11 +68,9 @@ class HomeFeed { * @deprecated Use getVideos instead */ get videos() { - if (this.#videos) return this.#videos; - const simplified = this.getVideos(); - this.#videos = simplified.map(video => { + return simplified.map(video => { return { id: video.id, title: video.title.toString(), @@ -79,12 +109,28 @@ class HomeFeed { } } }); - - return this.#videos; } - getContinuation() { - return; // TODO: implement this! + async getContinuation() { + 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]; + const response = await continuation.call(this.#session); + + return new HomeFeed(this.#session, response.response); + } + + this.#continuation = Simplify.matching({ + type: Simplify.matching(/^ContinuationItem$/), + }).runOn(this.#page); + + if (this.#continuation) + return this.getContinuation(); + + return; } } diff --git a/lib/parser/contents/ContinuationItem.js b/lib/parser/contents/ContinuationItem.js index 978503be7..d369948fe 100644 --- a/lib/parser/contents/ContinuationItem.js +++ b/lib/parser/contents/ContinuationItem.js @@ -1,12 +1,33 @@ const ResultsParser = require("."); +const Utils = require("../../utils/Utils"); const NavigationEndpoint = require("./NavigationEndpoint"); class ContinuationItem { type = 'ContinuationItem'; + is_resolved = false; + is_rejected = false; + pending_promise = null; + constructor(item) { this.endpoint = new NavigationEndpoint(item.continuationEndpoint); } + + async call(session) { + if (this.is_resolved || this.is_rejected) return; + this.pending_promise = this.endpoint.call(session); + const response = await this.pending_promise; + if (!response.success) { + this.is_rejected = true; + throw new Utils.InnertubeError('Could not retrieve continuation', response); + } + + this.response = ResultsParser.parseResponse(response.data); + + this.is_resolved = true; + + return this; + } } module.exports = ContinuationItem; \ No newline at end of file diff --git a/lib/parser/contents/NavigationEndpoint.js b/lib/parser/contents/NavigationEndpoint.js index 9e113716f..97a77a87c 100644 --- a/lib/parser/contents/NavigationEndpoint.js +++ b/lib/parser/contents/NavigationEndpoint.js @@ -1,8 +1,10 @@ const ResultsParser = require("."); +const { InnertubeError } = require("../../utils/Utils"); +const Actions = require("../../core/Actions"); class NavigationEndpoint { type = 'NavigationEndpoint'; - + constructor (item) { this.metadata = { api_url: item.commandMetadata.webCommandMetadata.api_url, @@ -63,6 +65,18 @@ class NavigationEndpoint { content: modalRenderer.content, } : null; } + + call(session) { + if (this.continuation) { + switch (this.continuation.request) { + case 'CONTINUATION_REQUEST_TYPE_BROWSE': + return Actions.browse(session, 'continuation', { ctoken: this.continuation.token }); + + default: + throw new InnertubeError(`Unknown continuation request type: ${this.continuation.request}`); + } + } + } } module.exports = NavigationEndpoint; \ No newline at end of file diff --git a/lib/parser/contents/index.js b/lib/parser/contents/index.js index d1c05009a..dda0e5023 100644 --- a/lib/parser/contents/index.js +++ b/lib/parser/contents/index.js @@ -1,10 +1,26 @@ +class AppendContinuationItemsAction { + type = 'AppendContinuationItemsAction'; + + constructor (item) { + this.continuation_items = ResultsParser.parse(item.continuationItems); + } +} + class ResultsParser { static parseResponse(data) { return { - contents: ResultsParser.parseItem(data.contents) + contents: data.contents && ResultsParser.parseItem(data.contents), + on_response_received_actions: data.onResponseReceivedActions && ResultsParser.parseRRA(data.onResponseReceivedActions) || undefined, } } + static parseRRA(actions) { + return actions.map((action) => { + if (Object.keys(action).includes('appendContinuationItemsAction')) + return new AppendContinuationItemsAction(action.appendContinuationItemsAction); + }).filter((item) => item); + } + static parse(contents) { return contents.map((item) => this.parseItem(item)).filter((item) => item); } diff --git a/typings/lib/Innertube.d.ts b/typings/lib/Innertube.d.ts index a5389d876..4b62546f2 100644 --- a/typings/lib/Innertube.d.ts +++ b/typings/lib/Innertube.d.ts @@ -373,17 +373,9 @@ declare class Innertube { }>; /** * Retrieves YouTube's home feed (aka recommendations). - * @returns {Promise.<{ videos: [{ id: string; title: string; description: string; channel: string; metadata: object }] }>} + * @returns {Promise} */ - getHomeFeed(): Promise<{ - videos: [{ - id: string; - title: string; - description: string; - channel: string; - metadata: object; - }]; - }>; + getHomeFeed(): Promise; /** * Retrieves trending content. * @returns {Promise.<{ now: { content: [{ title: string; videos: []; }] }; @@ -509,3 +501,4 @@ declare class Innertube { #private; } import Request = require("./utils/Request"); +import HomeFeed = require("./core/HomeFeed"); From ffe49ff468ccaff8566a96ec41f8b7aac6170733 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sun, 1 May 2022 11:43:20 +0200 Subject: [PATCH 15/55] fix: Actions moved to session This follows commit 1bfe2676d83628a458441e9acd9f31b20e820a68 --- lib/parser/contents/NavigationEndpoint.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/parser/contents/NavigationEndpoint.js b/lib/parser/contents/NavigationEndpoint.js index 97a77a87c..d168fa382 100644 --- a/lib/parser/contents/NavigationEndpoint.js +++ b/lib/parser/contents/NavigationEndpoint.js @@ -70,7 +70,7 @@ class NavigationEndpoint { if (this.continuation) { switch (this.continuation.request) { case 'CONTINUATION_REQUEST_TYPE_BROWSE': - return Actions.browse(session, 'continuation', { ctoken: this.continuation.token }); + return session.actions.browse(this.continuation.token, { is_ctoken: true }); default: throw new InnertubeError(`Unknown continuation request type: ${this.continuation.request}`); From 42f7eff676bc0b7510c3038b20ad320493f9ab23 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sun, 1 May 2022 11:57:46 +0200 Subject: [PATCH 16/55] fix: add more typescript config --- tsconfig.json | 5 +- typings/lib/Innertube.d.ts | 87 ++-- typings/lib/core/Actions.d.ts | 393 +++++++++++------- typings/lib/core/HomeFeed.d.ts | 46 +- typings/lib/core/Livechat.d.ts | 32 +- .../lib/parser/contents/ContinuationItem.d.ts | 8 + .../parser/contents/NavigationEndpoint.d.ts | 4 +- typings/lib/parser/contents/index.d.ts | 2 + typings/lib/proto/index.d.ts | 2 +- typings/lib/utils/Constants.d.ts | 13 +- typings/lib/utils/Request.d.ts | 2 +- typings/lib/utils/Utils.d.ts | 1 + 12 files changed, 364 insertions(+), 231 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 3dc714063..82db405bb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,10 @@ "declaration": true, "emitDeclarationOnly": true, "allowJs": true, - "outDir": "./typings" + "outDir": "./typings", + "lib": ["ESNext"], + "target": "ESNext", + "moduleResolution": "node" }, "include": [ "./lib/**/*.js", diff --git a/typings/lib/Innertube.d.ts b/typings/lib/Innertube.d.ts index 4b62546f2..968358cbf 100644 --- a/typings/lib/Innertube.d.ts +++ b/typings/lib/Innertube.d.ts @@ -5,9 +5,11 @@ declare class Innertube { * const Innertube = require('youtubei.js'); * const youtube = await new Innertube(); * ``` + * * @param {object} [config] * @param {string} [config.gl] * @param {string} [config.cookie] + * * @returns {Innertube} * @constructor */ @@ -26,13 +28,14 @@ declare class Innertube { logged_in: any; sts: any; /** - * @event Innertube#auth - Fired when signing in to an account. - * @event Innertube#update-credentials - Fired when the access token is no longer valid. + * @event Innertube#auth - fired when signing in to an account. + * @event Innertube#update-credentials - fired when the access token is no longer valid. * @type {EventEmitter} */ ev: EventEmitter; auth_apisid: any; request: Request; + actions: Actions; account: { info: () => Promise<{ name: string; @@ -208,6 +211,7 @@ declare class Innertube { * * @param {string} title * @param {string} video_ids + * * @returns {Promise.<{ success: boolean; status_code: string; playlist_id: string; }>} */ create: (title: string, video_ids: string) => Promise<{ @@ -268,7 +272,7 @@ declare class Innertube { access_token: any; refresh_token: any; /** - * Signs out of your account. + * Signs out of an account. * @returns {Promise.<{ success: boolean; status_code: number }>} */ signOut(): Promise<{ @@ -285,20 +289,26 @@ declare class Innertube { country: string; language: string; }>; + getTimeWatched(): Promise; /** * Searches on YouTube. * - * @param {string} query - Search query. - * @param {object} options - Search options. - * @param {string} option.params - Pre-defined search parameter. - * @param {string} options.client - Client used to perform the search, can be: `YTMUSIC` or `YOUTUBE`. - * @param {string} options.period - Filter videos uploaded within a period, can be: any | hour | day | week | month | year - * @param {string} options.order - Filter results by order, can be: relevance | rating | age | views - * @param {string} options.duration - Filter video results by duration, can be: any | short | long + * @param {string} query - search query. + * @param {object} options - search options. + * @param {string} options.client - client used to perform the search, can be: `YTMUSIC` or `YOUTUBE`. + * @param {string} options.period - filter videos uploaded within a period, can be: any | hour | day | week | month | year + * @param {string} options.order - filter results by order, can be: relevance | rating | age | views + * @param {string} options.duration - filter video results by duration, can be: any | short | long + * * @returns {Promise.<{ query: string; corrected_query: string; estimated_results: number; videos: [] } | * { results: { songs: []; videos: []; albums: []; community_playlists: [] } }>} */ - search(query: string, options?: object): Promise<{ + search(query: string, options?: { + client: string; + period: string; + order: string; + duration: string; + }): Promise<{ query: string; corrected_query: string; estimated_results: number; @@ -314,9 +324,10 @@ declare class Innertube { /** * Retrieves search suggestions. * - * @param {string} input - The search query. - * @param {object} [options] - Search options. - * @param {string} [options.client='YOUTUBE'] - Client used to retrieve search suggestions, can be: `YOUTUBE` or `YTMUSIC`. + * @param {string} input - the search query. + * @param {object} [options] - search options. + * @param {string} [options.client='YOUTUBE'] - client used to retrieve search suggestions, can be: `YOUTUBE` or `YTMUSIC`. + * * @returns {Promise.<[{ text: string; bold_text: string }]>} */ getSearchSuggestions(input: string, options?: { @@ -328,7 +339,7 @@ declare class Innertube { /** * Retrieves video info. * - * @param {string} video_id - Video id + * @param {string} video_id - video id * @return {Promise.<{ title: string; description: string; thumbnail: []; metadata: object }>} */ getDetails(video_id: string): Promise<{ @@ -340,8 +351,8 @@ declare class Innertube { /** * Retrieves comments for a video. * - * @param {string} video_id - Video id - * @param {string} [sort_by] - Can be: `TOP_COMMENTS` or `NEWEST_FIRST`. + * @param {string} video_id - video id + * @param {string} [sort_by] - can be: `TOP_COMMENTS` or `NEWEST_FIRST`. * @return {Promise.<{ page_count: number; comment_count: number; items: []; }>} */ getComments(video_id: string, sort_by?: string): Promise<{ @@ -352,7 +363,7 @@ declare class Innertube { /** * Retrieves contents for a given channel. (WIP) * - * @param {string} id - The id of the channel. + * @param {string} id - channel id * @return {Promise.<{ title: string; description: string; metadata: object; content: object }>} */ getChannel(id: string): Promise<{ @@ -362,7 +373,7 @@ declare class Innertube { content: object; }>; /** - * Retrieves your watch history. + * Retrieves watch history. * @returns {Promise.<{ items: [{ date: string; videos: [] }] }>} */ getHistory(): Promise<{ @@ -378,6 +389,7 @@ declare class Innertube { getHomeFeed(): Promise; /** * Retrieves trending content. + * * @returns {Promise.<{ now: { content: [{ title: string; videos: []; }] }; * music: { getVideos: Promise.; }; gaming: { getVideos: Promise.; }; * gaming: { getVideos: Promise.; }; }>} @@ -399,8 +411,9 @@ declare class Innertube { getVideos: Promise; }; }>; + getLibrary(): Promise; /** - * Retrieves your subscriptions feed. + * Retrieves subscriptions feed. * @returns {Promise.<{ items: [{ date: string; videos: [] }] }>} */ getSubscriptionsFeed(): Promise<{ @@ -427,22 +440,22 @@ declare class Innertube { }>; /** * Retrieves unseen notifications count. - * @returns {Promise.} unseen notifications count. + * @returns {Promise.} */ getUnseenNotificationsCount(): Promise; /** * Retrieves lyrics for a given song if available. * * @param {string} video_id - * @returns {Promise.} Song lyrics + * @returns {Promise.} */ getLyrics(video_id: string): Promise; /** - * Parses a given playlist. + * Retrieves a given playlist. * - * @param {string} playlist_id - The id of the playlist. + * @param {string} playlist_id - playlist id. * @param {object} options - { client: YOUTUBE | YTMUSIC } - * @param {string} options.client - Client used to parse the playlist, can be: `YTMUSIC` | `YOUTUBE` + * @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: [] } | * { title: string; description: string; total_items: number; duration: string; year: string; items: [] }>} @@ -468,11 +481,12 @@ declare class Innertube { * An alternative to {@link download}. * Returns deciphered streaming data. * - * @param {string} id - Video id - * @param {object} options - Download options. - * @param {string} options.quality - Video quality; 360p, 720p, 1080p, etc.... - * @param {string} options.type - Download type, can be: video, audio or videoandaudio - * @param {string} options.format - File format + * @param {string} id - video id + * @param {object} options - download options. + * @param {string} options.quality - video quality; 360p, 720p, 1080p, etc... + * @param {string} options.type - download type, can be: video, audio or videoandaudio + * @param {string} options.format - file format + * * @returns {Promise.<{ selected_format: {}; formats: [] }>} */ getStreamingData(id: string, options?: { @@ -486,11 +500,12 @@ declare class Innertube { /** * Downloads a given video. If you only need the direct download link take a look at {@link getStreamingData}. * - * @param {string} id - Video id - * @param {object} options - Download options. - * @param {string} options.quality - Video quality; 360p, 720p, 1080p, etc.... - * @param {string} options.type - Download type, can be: video, audio or videoandaudio - * @param {string} options.format - File format + * @param {string} id - video id + * @param {object} options - download options. + * @param {string} options.quality - video quality; 360p, 720p, 1080p, etc... + * @param {string} options.type - download type, can be: video, audio or videoandaudio + * @param {string} options.format - file format + * * @return {ReadableStream} */ download(id: string, options?: { @@ -500,5 +515,7 @@ declare class Innertube { }): ReadableStream; #private; } +import EventEmitter = require("events"); import Request = require("./utils/Request"); +import Actions = require("./core/Actions"); import HomeFeed = require("./core/HomeFeed"); diff --git a/typings/lib/core/Actions.d.ts b/typings/lib/core/Actions.d.ts index e2e8856ca..334f84e99 100644 --- a/typings/lib/core/Actions.d.ts +++ b/typings/lib/core/Actions.d.ts @@ -1,157 +1,236 @@ -/** - * Performs direct interactions on YouTube. - * - * @param {Innertube} session - * @param {string} engagement_type - * @param {object} args - * @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>} - */ -export function engage(session: Innertube, engagement_type: string, args?: object): Promise<{ - success: boolean; - status_code: number; - data: object; - message?: string; -}>; -/** - * Accesses YouTube's various sections. - * - * @param {Innertube} session - * @param {string} action - * @param {object} args - * @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>} - */ -export function browse(session: Innertube, action: string, args?: object): Promise<{ - success: boolean; - status_code: number; - data: object; - message?: string; -}>; -/** - * Account settings endpoints. - * - * @param {Innertube} session - * @param {string} action - * @param {object} args - * @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>} - */ -export function account(session: Innertube, action: string, args?: object): Promise<{ - success: boolean; - status_code: number; - data: object; - message?: string; -}>; -export function playlist(session: any, action: any, args?: {}): Promise<{ - success: boolean; - status_code: any; - message: string; - data?: undefined; -} | { - success: boolean; - status_code: any; - data: any; - message?: undefined; -}>; -/** - * Endpoints used to report content. - * - * @param {Innertube} session - * @param {string} action - * @param {object} args - * @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>} - */ -export function flag(session: Innertube, action: string, args?: object): Promise<{ - success: boolean; - status_code: number; - data: object; - message?: string; -}>; -/** - * Accesses YouTube Music endpoints (/youtubei/v1/music/). - * - * @param {Innertube} session - * @param {string} action - * @param {object} args - * @todo Implement more endpoints. - * @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>} - */ -export function music(session: Innertube, action: string, args: object): Promise<{ - success: boolean; - status_code: number; - data: object; - message?: string; -}>; -/** - * Searches a given query on YouTube/YTMusic. - * - * @param {Innertube} session - * @param {string} client - YouTube client: YOUTUBE | YTMUSIC - * @param {object} args - Search arguments. - * @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>} - */ -export function search(session: Innertube, client: string, args?: object): Promise<{ - success: boolean; - status_code: number; - data: object; - message?: string; -}>; -/** - * Interacts with YouTube's notification system. - * - * @param {Innertube} session - * @param {string} action - * @param {object} args - * @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>} - */ -export function notifications(session: Innertube, action: string, args?: object): Promise<{ - success: boolean; - status_code: number; - data: object; - message?: string; -}>; -/** - * Interacts with YouTube's livechat system. - * - * @param {Innertube} session - * @param {string} action - * @param {object} args - * @returns {Promise.<{ success: boolean; data: object; message?: string }>} - */ -export function livechat(session: Innertube, action: string, args?: object): Promise<{ - success: boolean; - data: object; - message?: string; -}>; -/** - * Retrieves video data. - * - * @param {Innertube} session - * @param {object} args - * @returns {Promise.} - Video data. - */ -export function getVideoInfo(session: Innertube, args?: object): Promise; -/** - * Requests continuation for previously performed actions. - * - * @param {Innertube} session - * @param {object} args - * @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>} - */ -export function next(session: Innertube, args?: object): Promise<{ - success: boolean; - status_code: number; - data: object; - message?: string; -}>; -/** - * Gets search suggestions. - * - * @param {Innertube} session - * @param {string} query - * @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>} - */ -export function getSearchSuggestions(session: Innertube, client: any, input: any): Promise<{ - success: boolean; - status_code: number; - data: object; - message?: string; -}>; +export = Actions; +declare class Actions { + constructor(session: any); + /** + * Covers Innertube's browse endpoint, mostly used to + * access YouTube's sections such as the home page + * and sometimes to retrieve continuations. + * + * @param {string} id - browseId or a continuation token + * @param {object} args - additional arguments + * @param {string} [args.params] + * @param {boolean} [args.is_ytm] + * @param {boolean} [args.is_ctoken] + * + * @returns {Promise.<{ success: boolean; status_code: number; data: object }>} + */ + browse(id: string, args?: { + params?: string; + is_ytm?: boolean; + is_ctoken?: boolean; + }): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Covers endpoints used to perform direct interactions + * on YouTube. + * + * @param {string} action + * @param {object} args + * @param {string} [args.video_id] + * @param {string} [args.channel_id] + * @param {string} [args.comment_id] + * @param {string} [args.comment_action] + * + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + engage(action: string, args?: { + video_id?: string; + channel_id?: string; + comment_id?: string; + comment_action?: string; + }): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Covers endpoints related to account management. + * + * @param {string} action + * @param {object} args + * @param {string} args.new_value + * @param {string} args.setting_item_id + * + * @returns {Promise.<{ success: boolean; status_code: number; data: object }>} + */ + account(action: string, args?: { + new_value: string; + setting_item_id: string; + }): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Covers the endpoint used for searches. + * + * @param {string} client + * @param {object} args + * @param {string} args.query + * @param {object} args.options + * @param {string} args.options.period + * @param {string} args.options.duration + * @param {string} args.options.order + * + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + search(client: string, args?: { + query: string; + options: { + period: string; + duration: string; + order: string; + }; + }): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Covers endpoints used for playlist management. + * + * @param {string} action + * @param {object} args + * @param {string} [args.title] + * @param {string} [args.ids] + * @param {string} [args.playlist_id] + * @param {string} [args.action] + * + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + playlist(action: string, args?: { + title?: string; + ids?: string; + playlist_id?: string; + action?: string; + }): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Covers endpoints used for notifications management. + * + * @param {string} action + * @param {object} args + * @param {string} [args.pref] + * @param {string} [args.channel_id] + * @param {string} [args.ctoken] + * + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + notifications(action: string, args: { + pref?: string; + channel_id?: string; + ctoken?: string; + }): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Covers livechat endpoints. + * + * @param {string} action + * @param {object} args + * @param {string} [args.text] + * @param {string} [args.video_id] + * @param {string} [args.channel_id] + * @param {string} [args.ctoken] + * @param {string} [args.params] + * + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + livechat(action: string, args?: { + text?: string; + video_id?: string; + channel_id?: string; + ctoken?: string; + params?: string; + }): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Covers endpoints used to report content. + * + * @param {string} action + * @param {object} args + * @param {object} [args.action] + * @param {string} [args.params] + * + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + flag(action: string, args: { + action?: object; + params?: string; + }): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Covers specific YouTube Music endpoints. + * + * @param {string} action + * @param {string} args.input + * + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + music(action: string, args: any): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Mostly used to retrieve data continuation for + * previously executed actions. + * + * @param {string} action + * @param {object} args + * @param {string} args.video_id + * @param {string} args.channel_id + * @param {string} args.ctoken + * @param {boolean} is_ytm + * + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + next(args?: { + video_id: string; + channel_id: string; + ctoken: string; + }): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Used to retrieve video info. + * + * @param {string} id + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + getVideoInfo(id: string): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Covers search suggestion endpoints. + * + * @param {string} client + * @param {string} input + * + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + getSearchSuggestions(client: string, input: string): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + #private; +} diff --git a/typings/lib/core/HomeFeed.d.ts b/typings/lib/core/HomeFeed.d.ts index ef42b8c0a..99d5375b4 100644 --- a/typings/lib/core/HomeFeed.d.ts +++ b/typings/lib/core/HomeFeed.d.ts @@ -1,22 +1,56 @@ export = HomeFeed; declare class HomeFeed { - constructor(data: any); + constructor(session: any, data: any); /** * Get the original page data */ - get page(): { - contents: any; - }; + get page(): any; /** * Get all the videos in the home feed * @returns {Array} */ getVideos(): Array; + /** + * Get filters for the home feed + * @returns {import('../parser/contents/ChipCloudChip')[]} + */ + getFeedFilters(): import('../parser/contents/ChipCloudChip')[]; /** * Get all the videos in the home feed * @deprecated Use getVideos instead */ - get videos(): any; - getContinuation(): void; + get videos(): { + id: string; + title: any; + description: string; + channel: { + id: any; + name: string; + url: any; + thumbnail: import("../parser/contents/Thumbnail"); + is_verified: boolean; + is_verified_artist: boolean; + }; + metadata: { + view_count: any; + short_view_count_text: { + simple_text: any; + accessibility_label: string; + }; + thumbnail: import("../parser/contents/Thumbnail"); + thumbnails: import("../parser/contents/Thumbnail")[]; + moving_thumbnail: {}; + moving_thumbnails: import("../parser/contents/Thumbnail")[]; + published: any; + duration: { + seconds: number; + simple_text: string; + accessibility_label: string; + }; + badges: import("../parser/contents/MetadataBadge")[]; + owner_badges: import("../parser/contents/MetadataBadge")[]; + }; + }[]; + getContinuation(): any; #private; } diff --git a/typings/lib/core/Livechat.d.ts b/typings/lib/core/Livechat.d.ts index 8b9f8a952..f709737f9 100644 --- a/typings/lib/core/Livechat.d.ts +++ b/typings/lib/core/Livechat.d.ts @@ -1,5 +1,5 @@ export = Livechat; -declare class Livechat { +declare class Livechat extends EventEmitter { constructor(session: any, token: any, channel_id: any, video_id: any); ctoken: any; session: any; @@ -10,33 +10,8 @@ declare class Livechat { poll_intervals_ms: number; running: boolean; metadata_ctoken: any; - livechat_poller: number; - sendMessage(text: any): Promise<{ - success: boolean; - data: any; - message?: string; - } | { - success: boolean; - status_code: any; - deleteMessage: () => Promise<{ - success: boolean; - data: any; - message?: string; - } | { - success: boolean; - status_code: any; - }>; - message_data: { - text: any; - author: { - name: any; - channel_id: any; - profile_picture: any; - }; - timestamp: any; - id: any; - }; - }>; + livechat_poller: NodeJS.Timeout; + sendMessage(text: any): Promise; /** * Blocks a user. * @todo Implement this method. @@ -46,3 +21,4 @@ declare class Livechat { stop(): void; #private; } +import EventEmitter = require("events"); diff --git a/typings/lib/parser/contents/ContinuationItem.d.ts b/typings/lib/parser/contents/ContinuationItem.d.ts index b9f95c50f..84cb4eee1 100644 --- a/typings/lib/parser/contents/ContinuationItem.d.ts +++ b/typings/lib/parser/contents/ContinuationItem.d.ts @@ -2,6 +2,14 @@ export = ContinuationItem; declare class ContinuationItem { constructor(item: any); type: string; + is_resolved: boolean; + is_rejected: boolean; + pending_promise: any; endpoint: NavigationEndpoint; + call(session: any): Promise; + response: { + contents: any; + on_response_received_actions: any; + }; } import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/NavigationEndpoint.d.ts b/typings/lib/parser/contents/NavigationEndpoint.d.ts index ecd54edc6..47dfc2066 100644 --- a/typings/lib/parser/contents/NavigationEndpoint.d.ts +++ b/typings/lib/parser/contents/NavigationEndpoint.d.ts @@ -1,3 +1,4 @@ +/// export = NavigationEndpoint; declare class NavigationEndpoint { constructor(item: any); @@ -32,7 +33,7 @@ declare class NavigationEndpoint { sequence_params: any; }; url: { - url: URL; + url: import("url").URL; target: any; nofollow: any; }; @@ -47,4 +48,5 @@ declare class NavigationEndpoint { button: any; content: any; }; + call(session: any): any; } diff --git a/typings/lib/parser/contents/index.d.ts b/typings/lib/parser/contents/index.d.ts index 0762a3370..734fc4e82 100644 --- a/typings/lib/parser/contents/index.d.ts +++ b/typings/lib/parser/contents/index.d.ts @@ -2,7 +2,9 @@ export = ResultsParser; declare class ResultsParser { static parseResponse(data: any): { contents: any; + on_response_received_actions: any; }; + static parseRRA(actions: any): any; static parse(contents: any): any; static parseItem(item: any): any; } diff --git a/typings/lib/proto/index.d.ts b/typings/lib/proto/index.d.ts index b195e0012..98c5547cf 100644 --- a/typings/lib/proto/index.d.ts +++ b/typings/lib/proto/index.d.ts @@ -1,7 +1,7 @@ export = Proto; declare class Proto { static encodeSearchFilter(period: any, duration: any, order: any): string; - static encodeMessageParams(channel_id: any, video_id: any): any; + static encodeMessageParams(channel_id: any, video_id: any): string; static encodeCommentsSectionParams(video_id: any, options?: {}): string; static encodeCommentRepliesParams(video_id: any, comment_id: any): string; static encodeCommentParams(video_id: any): string; diff --git a/typings/lib/utils/Constants.d.ts b/typings/lib/utils/Constants.d.ts index 0ba75b8ce..24b7604a8 100644 --- a/typings/lib/utils/Constants.d.ts +++ b/typings/lib/utils/Constants.d.ts @@ -25,6 +25,18 @@ export namespace OAUTH { const CLIENT_IDENTITY: RegExp; } } +export namespace CLIENTS { + namespace YTMUSIC { + const NAME: string; + const VERSION: string; + } + namespace ANDROID { + const NAME_1: string; + export { NAME_1 as NAME }; + const VERSION_1: string; + export { VERSION_1 as VERSION }; + } +} export function DEFAULT_HEADERS(config: any): { headers: { Cookie: any; @@ -64,7 +76,6 @@ export function VIDEO_INFO_REQBODY(id: any, sts: any, context: any): { context: any; videoId: any; }; -export const YTMUSIC_VERSION: string; export const METADATA_KEYS: string[]; export const BLACKLISTED_KEYS: string[]; export namespace ACCOUNT_SETTINGS { diff --git a/typings/lib/utils/Request.d.ts b/typings/lib/utils/Request.d.ts index 97b7f0331..fc431022e 100644 --- a/typings/lib/utils/Request.d.ts +++ b/typings/lib/utils/Request.d.ts @@ -2,6 +2,6 @@ export = Request; declare class Request { constructor(session: any); session: any; - instance: any; + instance: import("axios").AxiosInstance; #private; } diff --git a/typings/lib/utils/Utils.d.ts b/typings/lib/utils/Utils.d.ts index 43f8b6d91..2c4650abe 100644 --- a/typings/lib/utils/Utils.d.ts +++ b/typings/lib/utils/Utils.d.ts @@ -9,6 +9,7 @@ export class InnertubeError { constructor(message: any, info: any); info: any; stack: string; + message: any; constructor: typeof InnertubeError; } export class MissingParamError extends InnertubeError { From 5a8b13d57c6f6200a58bc2ccb6b83bba1e083192 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sun, 1 May 2022 12:22:32 +0200 Subject: [PATCH 17/55] chore: use correct spaces and quotes --- lib/parser/contents/Author.js | 86 ++++---- lib/parser/contents/BackstageImage.js | 12 +- lib/parser/contents/BackstagePost.js | 38 ++-- lib/parser/contents/BackstagePostThread.js | 10 +- lib/parser/contents/Button.js | 36 ++-- lib/parser/contents/Channel.js | 32 +-- .../contents/ChannelAboutFullMetadata.js | 32 +-- lib/parser/contents/ChannelMetadata.js | 38 ++-- lib/parser/contents/ChannelVideoPlayer.js | 20 +- lib/parser/contents/ChildVideo.js | 20 +- lib/parser/contents/ChipCloudChip.js | 18 +- lib/parser/contents/CollageHeroImage.js | 20 +- lib/parser/contents/CommentActionButtons.js | 14 +- lib/parser/contents/CompactVideo.js | 34 ++-- lib/parser/contents/ContinuationItem.js | 44 ++--- lib/parser/contents/ExpandableTab.js | 18 +- lib/parser/contents/GenericContainer.js | 12 +- lib/parser/contents/GenericList.js | 14 +- lib/parser/contents/GridChannel.js | 26 +-- lib/parser/contents/GridPlaylist.js | 28 +-- lib/parser/contents/GridVideo.js | 36 ++-- lib/parser/contents/HorizontalCardList.js | 14 +- lib/parser/contents/HorizontalList.js | 12 +- lib/parser/contents/Menu.js | 12 +- lib/parser/contents/MenuNavigationItem.js | 16 +- lib/parser/contents/MenuServiceItem.js | 18 +- lib/parser/contents/MetadataBadge.js | 20 +- lib/parser/contents/MetadataRow.js | 14 +- lib/parser/contents/MetadataRowContainer.js | 10 +- lib/parser/contents/MetadataRowHeader.js | 12 +- lib/parser/contents/Mix.js | 18 +- lib/parser/contents/MovingThumbnail.js | 12 +- lib/parser/contents/NavigatableText.js | 38 ++-- lib/parser/contents/NavigationEndpoint.js | 144 +++++++------- lib/parser/contents/Playlist.js | 34 ++-- lib/parser/contents/PlaylistPanelVideo.js | 32 +-- lib/parser/contents/PlaylistVideo.js | 32 +-- lib/parser/contents/PlaylistVideoList.js | 16 +- lib/parser/contents/ReelItem.js | 24 +-- lib/parser/contents/ReelShelf.js | 18 +- lib/parser/contents/RichGrid.js | 16 +- lib/parser/contents/RichShelf.js | 18 +- lib/parser/contents/SearchRefinementCard.js | 24 +-- lib/parser/contents/Shelf.js | 32 +-- lib/parser/contents/Tab.js | 18 +- lib/parser/contents/Text.js | 54 ++--- lib/parser/contents/TextRun.js | 18 +- lib/parser/contents/Thumbnail.js | 34 ++-- lib/parser/contents/ToggleButton.js | 72 +++---- lib/parser/contents/TwoColumnBrowseResults.js | 10 +- lib/parser/contents/TwoColumnSearchResults.js | 14 +- .../contents/TwoColumnWatchNextResults.js | 54 ++--- lib/parser/contents/UniversalWatchCard.js | 14 +- lib/parser/contents/VerticalList.js | 12 +- lib/parser/contents/Video.js | 10 +- lib/parser/contents/VideoOwner.js | 12 +- lib/parser/contents/VideoPrimaryInfo.js | 20 +- lib/parser/contents/VideoSecondaryInfo.js | 18 +- lib/parser/contents/WatchCardCompactVideo.js | 30 +-- lib/parser/contents/WatchCardHeroVideo.js | 14 +- lib/parser/contents/WatchCardRichHeader.js | 20 +- .../contents/WatchCardSectionSequence.js | 12 +- lib/parser/contents/index.js | 186 +++++++++--------- 63 files changed, 898 insertions(+), 898 deletions(-) diff --git a/lib/parser/contents/Author.js b/lib/parser/contents/Author.js index 479a1b9a1..071b950ec 100644 --- a/lib/parser/contents/Author.js +++ b/lib/parser/contents/Author.js @@ -1,66 +1,66 @@ -const ResultsParser = require("."); -const NavigatableText = require("./NavigatableText"); -const Thumbnail = require("./Thumbnail"); +const ResultsParser = require('.'); +const NavigatableText = require('./NavigatableText'); +const Thumbnail = require('./Thumbnail'); class Author { - #nav_text; - /** + #nav_text; + /** * @type {import('./MetadataBadge')[]} */ - badges; - /** + badges; + /** * @type {Thumbnail[]} */ - thumbnails; - constructor(item, badges, thumbs) { - this.#nav_text = new NavigatableText(item); - this.badges = Array.isArray(badges) ? ResultsParser.parse(badges) : []; - if (thumbs) { - this.thumbnails = Thumbnail.fromResponse(thumbs); - } - else { - this.thumbnails = []; - } + thumbnails; + constructor(item, badges, thumbs) { + this.#nav_text = new NavigatableText(item); + this.badges = Array.isArray(badges) ? ResultsParser.parse(badges) : []; + if (thumbs) { + this.thumbnails = Thumbnail.fromResponse(thumbs); } - - get name() { - return this.#nav_text.toString(); + else { + this.thumbnails = []; } + } - set name(name) { - this.#nav_text.text = name; - } + get name() { + return this.#nav_text.toString(); + } - get endpoint() { - return this.#nav_text.endpoint; - } + set name(name) { + this.#nav_text.text = name; + } - get id() { - // XXX: maybe confirm that pageType == "WEB_PAGE_TYPE_CHANNEL"? - // TODO: this is outdated - return this.#nav_text.endpoint.browseId; - } + 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'); - } + 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'); - } + get is_verified_artist() { + return this.badges.some(badge => badge.style === 'BADGE_STYLE_TYPE_VERIFIED_ARTIST'); + } - /** + /** * @type {Thumbnail | undefined} */ - get best_thumbnail() { - return this.thumbnails[0]; - } + get best_thumbnail() { + return this.thumbnails[0]; + } } module.exports = Author; \ No newline at end of file diff --git a/lib/parser/contents/BackstageImage.js b/lib/parser/contents/BackstageImage.js index 7967e0bfc..0edbe0c8b 100644 --- a/lib/parser/contents/BackstageImage.js +++ b/lib/parser/contents/BackstageImage.js @@ -1,12 +1,12 @@ -const ResultsParser = require("."); -const Thumbnail = require("./Thumbnail"); +const ResultsParser = require('.'); +const Thumbnail = require('./Thumbnail'); class BackstageImage { - type = 'BackstageImage'; + type = 'BackstageImage'; - constructor(item) { - this.image = Thumbnail.fromResponse(item.image); - } + constructor(item) { + this.image = Thumbnail.fromResponse(item.image); + } } module.exports = BackstageImage; \ No newline at end of file diff --git a/lib/parser/contents/BackstagePost.js b/lib/parser/contents/BackstagePost.js index be9b2044b..e74e46d4b 100644 --- a/lib/parser/contents/BackstagePost.js +++ b/lib/parser/contents/BackstagePost.js @@ -1,26 +1,26 @@ -const ResultsParser = require("."); -const Author = require("./Author"); -const Text = require("./Text"); +const ResultsParser = require('.'); +const Author = require('./Author'); +const Text = require('./Text'); class BackstagePost { - type = 'BackstagePost'; + type = 'BackstagePost'; - constructor(item) { - this.id = item.postId; - this.author = new Author({ - ...item.authorText, - navigationEndpoint: item.authorEndpoint - }, null, item.authorThumbnail); - this.content = new Text(item.contentText, ''); - this.published_at = new Text(item.publishedTimeText); - this.likes = new Text(item.voteCount); - this.actions = ResultsParser.parseItem(item.actionButtons); - this.attachment = item.backstageAttachment ? ResultsParser.parseItem(item.backstageAttachment) : null; - } + constructor(item) { + this.id = item.postId; + this.author = new Author({ + ...item.authorText, + navigationEndpoint: item.authorEndpoint + }, null, item.authorThumbnail); + this.content = new Text(item.contentText, ''); + this.published_at = new Text(item.publishedTimeText); + this.likes = new Text(item.voteCount); + this.actions = ResultsParser.parseItem(item.actionButtons); + this.attachment = item.backstageAttachment ? ResultsParser.parseItem(item.backstageAttachment) : null; + } - get endpoint() { - return this.actions.reply.endpoint; - } + get endpoint() { + return this.actions.reply.endpoint; + } } module.exports = BackstagePost; \ No newline at end of file diff --git a/lib/parser/contents/BackstagePostThread.js b/lib/parser/contents/BackstagePostThread.js index 2d9d4c112..a85e6fb11 100644 --- a/lib/parser/contents/BackstagePostThread.js +++ b/lib/parser/contents/BackstagePostThread.js @@ -1,11 +1,11 @@ -const ResultsParser = require("."); +const ResultsParser = require('.'); class BackstagePostThread { - type = 'BackstagePostThread'; + type = 'BackstagePostThread'; - constructor(item) { - this.post = ResultsParser.parseItem(item.post); - } + constructor(item) { + this.post = ResultsParser.parseItem(item.post); + } } module.exports = BackstagePostThread; \ No newline at end of file diff --git a/lib/parser/contents/Button.js b/lib/parser/contents/Button.js index 15fbacb67..f73538b4b 100644 --- a/lib/parser/contents/Button.js +++ b/lib/parser/contents/Button.js @@ -1,26 +1,26 @@ -const ResultsParser = require("."); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); +const ResultsParser = require('.'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); class Button { - type = 'Button'; + type = 'Button'; - constructor(item) { - this.navigation_endpoint = item.navigationEndpoint && new NavigationEndpoint(item.navigationEndpoint); - this.service_endpoint = item.serviceEndpoint && new NavigationEndpoint(item.serviceEndpoint); - this.text = new Text(item.text); - this.tooltip = item.tooltip; - } + constructor(item) { + this.navigation_endpoint = item.navigationEndpoint && new NavigationEndpoint(item.navigationEndpoint); + this.service_endpoint = item.serviceEndpoint && new NavigationEndpoint(item.serviceEndpoint); + this.text = new Text(item.text); + this.tooltip = item.tooltip; + } - get endpoint() { - if (this.service_endpoint) { - return this.service_endpoint; - } - if (this.navigation_endpoint) { - return this.navigation_endpoint; - } - return null; + get endpoint() { + if (this.service_endpoint) { + return this.service_endpoint; + } + if (this.navigation_endpoint) { + return this.navigation_endpoint; } + return null; + } } module.exports = Button; \ No newline at end of file diff --git a/lib/parser/contents/Channel.js b/lib/parser/contents/Channel.js index 582f753df..f15dba951 100644 --- a/lib/parser/contents/Channel.js +++ b/lib/parser/contents/Channel.js @@ -1,22 +1,22 @@ -const ResultsParser = require("."); -const Author = require("./Author"); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); +const ResultsParser = require('.'); +const Author = require('./Author'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); class Channel { - type = 'Channel'; + type = 'Channel'; - constructor(item) { - this.id = item.channelId; - this.author = new Author({ - ...item.title, - navigationEndpoint: item.navigationEndpoint - }, item.ownerBadges, item.thumbnail); - this.subscribers = new Text(item.subscriberCountText); - this.description_snippet = new Text(item.descriptionSnippet); - this.videos = new Text(item.videoCountText); - this.endpoint = new NavigationEndpoint(item.navigationEndpoint); - } + constructor(item) { + this.id = item.channelId; + this.author = new Author({ + ...item.title, + navigationEndpoint: item.navigationEndpoint + }, item.ownerBadges, item.thumbnail); + this.subscribers = new Text(item.subscriberCountText); + this.description_snippet = new Text(item.descriptionSnippet); + this.videos = new Text(item.videoCountText); + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + } } module.exports = Channel; \ No newline at end of file diff --git a/lib/parser/contents/ChannelAboutFullMetadata.js b/lib/parser/contents/ChannelAboutFullMetadata.js index 395663dfb..b03898d29 100644 --- a/lib/parser/contents/ChannelAboutFullMetadata.js +++ b/lib/parser/contents/ChannelAboutFullMetadata.js @@ -1,22 +1,22 @@ -const ResultsParser = require("."); -const Author = require("./Author"); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); +const ResultsParser = require('.'); +const Author = require('./Author'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); class ChannelAboutFullMetadata { - type = 'ChannelAboutFullMetadata'; + type = 'ChannelAboutFullMetadata'; - constructor(item) { - this.id = item.channelId; - this.canonical_channel_url = item.canonicalChannelUrl; - this.author = new Author(item.title, null, item.avatar); - this.views = new Text(item.viewCountText); - this.joined = new Text(item.joinedDateText); - this.description = new Text(item.description); - this.email_reveal = new NavigationEndpoint(item.onBusinessEmailRevealClickCommand); - this.can_reveal_email = !item.signInForBusinessEmail; - this.country = new Text(item.country); - } + constructor(item) { + this.id = item.channelId; + this.canonical_channel_url = item.canonicalChannelUrl; + this.author = new Author(item.title, null, item.avatar); + this.views = new Text(item.viewCountText); + this.joined = new Text(item.joinedDateText); + this.description = new Text(item.description); + this.email_reveal = new NavigationEndpoint(item.onBusinessEmailRevealClickCommand); + this.can_reveal_email = !item.signInForBusinessEmail; + this.country = new Text(item.country); + } } module.exports = ChannelAboutFullMetadata; \ No newline at end of file diff --git a/lib/parser/contents/ChannelMetadata.js b/lib/parser/contents/ChannelMetadata.js index 8a47da085..6633bec48 100644 --- a/lib/parser/contents/ChannelMetadata.js +++ b/lib/parser/contents/ChannelMetadata.js @@ -1,26 +1,26 @@ -const ResultsParser = require("."); -const Thumbnail = require("./Thumbnail"); +const ResultsParser = require('.'); +const Thumbnail = require('./Thumbnail'); class ChannelMetadata { - type = 'ChannelMetadata'; + type = 'ChannelMetadata'; - constructor(item) { - this.title = item.title; - this.description = item.description; - this.metadata = { - url: item.url, - rss_urls: item.rssUrl, - vanity_channel_url: item.vanityChannelUrl, - external_id: item.externalId, - is_family_safe: item.isFamilySafe, - keywords: item.keywords, - avatar: Thumbnail.fromResponse(item.avatar), - available_countries: item.availableCountryCodes, - android_deep_link: item.androidDeepLink, - android_appindexing_link: item.androidAppIndexingLink, - ios_appindexing_link: item.iosAppIndexingLink, - } + constructor(item) { + this.title = item.title; + this.description = item.description; + this.metadata = { + url: item.url, + rss_urls: item.rssUrl, + vanity_channel_url: item.vanityChannelUrl, + external_id: item.externalId, + is_family_safe: item.isFamilySafe, + keywords: item.keywords, + avatar: Thumbnail.fromResponse(item.avatar), + available_countries: item.availableCountryCodes, + android_deep_link: item.androidDeepLink, + android_appindexing_link: item.androidAppIndexingLink, + ios_appindexing_link: item.iosAppIndexingLink, } + } } module.exports = ChannelMetadata; \ No newline at end of file diff --git a/lib/parser/contents/ChannelVideoPlayer.js b/lib/parser/contents/ChannelVideoPlayer.js index 2eaa75b9c..ffad875e2 100644 --- a/lib/parser/contents/ChannelVideoPlayer.js +++ b/lib/parser/contents/ChannelVideoPlayer.js @@ -1,16 +1,16 @@ -const ResultsParser = require("."); -const Text = require("./Text"); +const ResultsParser = require('.'); +const Text = require('./Text'); class ChannelVideoPlayer { - type = 'ChannelVideoPlayer'; + type = 'ChannelVideoPlayer'; - constructor(item) { - this.id = item.videoId; - this.title = new Text(item.title, ''); - this.description = new Text(item.description, ''); - this.views = new Text(item.viewCountText, ''); - this.published_at = new Text(item.publishedTimeText, ''); - } + constructor(item) { + this.id = item.videoId; + this.title = new Text(item.title, ''); + this.description = new Text(item.description, ''); + this.views = new Text(item.viewCountText, ''); + this.published_at = new Text(item.publishedTimeText, ''); + } } module.exports = ChannelVideoPlayer; \ No newline at end of file diff --git a/lib/parser/contents/ChildVideo.js b/lib/parser/contents/ChildVideo.js index c189fd3d2..bf207352b 100644 --- a/lib/parser/contents/ChildVideo.js +++ b/lib/parser/contents/ChildVideo.js @@ -1,16 +1,16 @@ -const ResultsParser = require("."); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); +const ResultsParser = require('.'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); class ChildVideo { - type = 'ChildVideo'; + type = 'ChildVideo'; - constructor(item) { - this.id = item.videoId; - this.title = new Text(item.title); - this.length = new Text(item.lengthText); - this.endpoint = new NavigationEndpoint(item.navigationEndpoint); - } + constructor(item) { + this.id = item.videoId; + this.title = new Text(item.title); + this.length = new Text(item.lengthText); + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + } } module.exports = ChildVideo; \ No newline at end of file diff --git a/lib/parser/contents/ChipCloudChip.js b/lib/parser/contents/ChipCloudChip.js index 20e7ef56b..2844c3f4e 100644 --- a/lib/parser/contents/ChipCloudChip.js +++ b/lib/parser/contents/ChipCloudChip.js @@ -1,15 +1,15 @@ -const ResultsParser = require("."); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); +const ResultsParser = require('.'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); class ChipCloudChip { - type = 'ChipCloudChip'; + type = 'ChipCloudChip'; - constructor(item) { - this.selected = item.selected; - this.endpoint = item.navigationEndpoint && new NavigationEndpoint(item.navigationEndpoint); - this.text = new Text(item.text); - } + constructor(item) { + this.selected = item.selected; + this.endpoint = item.navigationEndpoint && new NavigationEndpoint(item.navigationEndpoint); + this.text = new Text(item.text); + } } module.exports = ChipCloudChip; \ No newline at end of file diff --git a/lib/parser/contents/CollageHeroImage.js b/lib/parser/contents/CollageHeroImage.js index cb3063fb6..20a24aea0 100644 --- a/lib/parser/contents/CollageHeroImage.js +++ b/lib/parser/contents/CollageHeroImage.js @@ -1,16 +1,16 @@ -const ResultsParser = require("."); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Thumbnail = require("./Thumbnail"); +const ResultsParser = require('.'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Thumbnail = require('./Thumbnail'); class CollageHeroImage { - type = 'CollageHeroImage'; + type = 'CollageHeroImage'; - constructor(item) { - this.left = Thumbnail.fromResponse(item.leftThumbnail); - this.top_right = Thumbnail.fromResponse(item.topRightThumbnail); - this.bottom_right = Thumbnail.fromResponse(item.bottomRightThumbnail); - this.endpoint = new NavigationEndpoint(item.navigationEndpoint); - } + constructor(item) { + this.left = Thumbnail.fromResponse(item.leftThumbnail); + this.top_right = Thumbnail.fromResponse(item.topRightThumbnail); + this.bottom_right = Thumbnail.fromResponse(item.bottomRightThumbnail); + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + } } module.exports = CollageHeroImage; \ No newline at end of file diff --git a/lib/parser/contents/CommentActionButtons.js b/lib/parser/contents/CommentActionButtons.js index 56356ba3b..52a1eded1 100644 --- a/lib/parser/contents/CommentActionButtons.js +++ b/lib/parser/contents/CommentActionButtons.js @@ -1,13 +1,13 @@ -const ResultsParser = require("."); +const ResultsParser = require('.'); class CommentActionButtons { - type = 'CommentActionButtons'; + type = 'CommentActionButtons'; - constructor(item) { - this.like = ResultsParser.parseItem(item.likeButton); - this.reply = ResultsParser.parseItem(item.replyButton); - this.dislike = ResultsParser.parseItem(item.dislikeButton); - } + constructor(item) { + this.like = ResultsParser.parseItem(item.likeButton); + this.reply = ResultsParser.parseItem(item.replyButton); + this.dislike = ResultsParser.parseItem(item.dislikeButton); + } } module.exports = CommentActionButtons; \ No newline at end of file diff --git a/lib/parser/contents/CompactVideo.js b/lib/parser/contents/CompactVideo.js index c5eebd789..e02797eeb 100644 --- a/lib/parser/contents/CompactVideo.js +++ b/lib/parser/contents/CompactVideo.js @@ -1,23 +1,23 @@ -const ResultsParser = require("."); -const Author = require("./Author"); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); -const Thumbnail = require("./Thumbnail"); +const ResultsParser = require('.'); +const Author = require('./Author'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); +const Thumbnail = require('./Thumbnail'); class CompactVideo { - type = 'CompactVideo'; + type = 'CompactVideo'; - constructor(item) { - this.id = item.videoId; - this.thumbnails = Thumbnail.fromResponse(item.thumbnail); - this.rich_thumbnail = item.richThumbnail && ResultsParser.parseItem(item.richThumbnail); - this.title = new Text(item.title); - this.author = new Author(item.longBylineText, item.ownerBadges, item.channelThumbnail); - this.published_at = new Text(item.publishedTimeText); - this.views = new Text(item.viewCountText); - this.duration = new Text(item.lengthText); - this.endpoint = new NavigationEndpoint(item.navigationEndpoint); - } + constructor(item) { + this.id = item.videoId; + this.thumbnails = Thumbnail.fromResponse(item.thumbnail); + this.rich_thumbnail = item.richThumbnail && ResultsParser.parseItem(item.richThumbnail); + this.title = new Text(item.title); + this.author = new Author(item.longBylineText, item.ownerBadges, item.channelThumbnail); + this.published_at = new Text(item.publishedTimeText); + this.views = new Text(item.viewCountText); + this.duration = new Text(item.lengthText); + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + } } module.exports = CompactVideo; \ No newline at end of file diff --git a/lib/parser/contents/ContinuationItem.js b/lib/parser/contents/ContinuationItem.js index d369948fe..266c75c3f 100644 --- a/lib/parser/contents/ContinuationItem.js +++ b/lib/parser/contents/ContinuationItem.js @@ -1,33 +1,33 @@ -const ResultsParser = require("."); -const Utils = require("../../utils/Utils"); -const NavigationEndpoint = require("./NavigationEndpoint"); +const ResultsParser = require('.'); +const Utils = require('../../utils/Utils'); +const NavigationEndpoint = require('./NavigationEndpoint'); class ContinuationItem { - type = 'ContinuationItem'; + type = 'ContinuationItem'; - is_resolved = false; - is_rejected = false; - pending_promise = null; + is_resolved = false; + is_rejected = false; + pending_promise = null; - constructor(item) { - this.endpoint = new NavigationEndpoint(item.continuationEndpoint); - } + constructor(item) { + this.endpoint = new NavigationEndpoint(item.continuationEndpoint); + } - async call(session) { - if (this.is_resolved || this.is_rejected) return; - this.pending_promise = this.endpoint.call(session); - const response = await this.pending_promise; - if (!response.success) { - this.is_rejected = true; - throw new Utils.InnertubeError('Could not retrieve continuation', response); - } + async call(session) { + if (this.is_resolved || this.is_rejected) return; + this.pending_promise = this.endpoint.call(session); + const response = await this.pending_promise; + if (!response.success) { + this.is_rejected = true; + throw new Utils.InnertubeError('Could not retrieve continuation', response); + } - this.response = ResultsParser.parseResponse(response.data); + this.response = ResultsParser.parseResponse(response.data); - this.is_resolved = true; + this.is_resolved = true; - return this; - } + return this; + } } module.exports = ContinuationItem; \ No newline at end of file diff --git a/lib/parser/contents/ExpandableTab.js b/lib/parser/contents/ExpandableTab.js index 1da4d98d6..b5e0587f7 100644 --- a/lib/parser/contents/ExpandableTab.js +++ b/lib/parser/contents/ExpandableTab.js @@ -1,15 +1,15 @@ -const ResultsParser = require("."); -const NavigationEndpoint = require("./NavigationEndpoint"); +const ResultsParser = require('.'); +const NavigationEndpoint = require('./NavigationEndpoint'); class ExpandableTab { - type = 'ExpandableTab'; + type = 'ExpandableTab'; - constructor(item) { - this.title = item.title; - this.endpoint = new NavigationEndpoint(item.endpoint); - this.selected = item.selected; // if this.selected then we may have content else we do not - this.content = item.content ? ResultsParser.parseItem(item.content) : null; - } + constructor(item) { + this.title = item.title; + this.endpoint = new NavigationEndpoint(item.endpoint); + this.selected = item.selected; // if this.selected then we may have content else we do not + this.content = item.content ? ResultsParser.parseItem(item.content) : null; + } } module.exports = ExpandableTab; \ No newline at end of file diff --git a/lib/parser/contents/GenericContainer.js b/lib/parser/contents/GenericContainer.js index 31c05e20c..d2e59015e 100644 --- a/lib/parser/contents/GenericContainer.js +++ b/lib/parser/contents/GenericContainer.js @@ -1,11 +1,11 @@ -const ResultsParser = require("."); +const ResultsParser = require('.'); module.exports = (name) => { - return class GenericContainer { - type = name; + return class GenericContainer { + type = name; - constructor(item) { - this.content = ResultsParser.parseItem(item.content); - } + constructor(item) { + this.content = ResultsParser.parseItem(item.content); } + } }; \ No newline at end of file diff --git a/lib/parser/contents/GenericList.js b/lib/parser/contents/GenericList.js index be3afa2cd..390dfb33b 100644 --- a/lib/parser/contents/GenericList.js +++ b/lib/parser/contents/GenericList.js @@ -1,12 +1,12 @@ -const ResultsParser = require("."); +const ResultsParser = require('.'); module.exports = (name, field = 'contents') => { - return class List { - type = name; - is_list = true; - constructor(items) { - this.contents = ResultsParser.parse(items[field]); - } + return class List { + type = name; + is_list = true; + constructor(items) { + this.contents = ResultsParser.parse(items[field]); } + } } \ No newline at end of file diff --git a/lib/parser/contents/GridChannel.js b/lib/parser/contents/GridChannel.js index 6d266a773..c80638a74 100644 --- a/lib/parser/contents/GridChannel.js +++ b/lib/parser/contents/GridChannel.js @@ -1,19 +1,19 @@ -const ResultsParser = require("."); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); -const Thumbnail = require("./Thumbnail"); +const ResultsParser = require('.'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); +const Thumbnail = require('./Thumbnail'); class GridChannel { - type = 'GridChannel'; + type = 'GridChannel'; - constructor(item) { - this.id = item.channelId; - this.thumbnails = Thumbnail.fromResponse(item.thumbnail); - this.videos = new Text(item.videoCountText); - this.subscribers = new Text(item.subscriberCountText); - this.name = new Text(item.title); - this.endpoint = new NavigationEndpoint(item.navigationEndpoint); - } + constructor(item) { + this.id = item.channelId; + this.thumbnails = Thumbnail.fromResponse(item.thumbnail); + this.videos = new Text(item.videoCountText); + this.subscribers = new Text(item.subscriberCountText); + this.name = new Text(item.title); + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + } } module.exports = GridChannel; \ No newline at end of file diff --git a/lib/parser/contents/GridPlaylist.js b/lib/parser/contents/GridPlaylist.js index 12f6d38f7..a9c9d23e7 100644 --- a/lib/parser/contents/GridPlaylist.js +++ b/lib/parser/contents/GridPlaylist.js @@ -1,20 +1,20 @@ -const ResultsParser = require("."); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); -const Thumbnail = require("./Thumbnail"); +const ResultsParser = require('.'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); +const Thumbnail = require('./Thumbnail'); class GridPlaylist { - type = 'GridPlaylist'; + type = 'GridPlaylist'; - constructor(item) { - this.id = item.playlistId; - this.videos = new Text(item.videoCountShortText); - this.thumbnauls = Thumbnail.fromResponse(item.thumbnail); - this.video_thumbnails = Array.isArray(item.sidebarThumbnails) ? item.sidebarThumbnails.map(thumbs => Thumbnail.fromResponse(thumbs)) : []; - this.title = new Text(item.title); - this.endpoint = new NavigationEndpoint(item.navigationEndpoint); - this.view_playlist = new Text(item.viewPlaylistText); - } + constructor(item) { + this.id = item.playlistId; + this.videos = new Text(item.videoCountShortText); + this.thumbnauls = Thumbnail.fromResponse(item.thumbnail); + this.video_thumbnails = Array.isArray(item.sidebarThumbnails) ? item.sidebarThumbnails.map(thumbs => Thumbnail.fromResponse(thumbs)) : []; + this.title = new Text(item.title); + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + this.view_playlist = new Text(item.viewPlaylistText); + } } module.exports = GridPlaylist; \ No newline at end of file diff --git a/lib/parser/contents/GridVideo.js b/lib/parser/contents/GridVideo.js index 41cbca658..9cb1fbbbc 100644 --- a/lib/parser/contents/GridVideo.js +++ b/lib/parser/contents/GridVideo.js @@ -1,24 +1,24 @@ -const ResultsParser = require("."); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); -const Thumbnail = require("./Thumbnail"); +const ResultsParser = require('.'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); +const Thumbnail = require('./Thumbnail'); class GridVideo { - type = 'GridVideo'; + type = 'GridVideo'; - constructor(item) { - this.id = item.videoId; - this.thumbnails = Thumbnail.fromResponse(item.thumbnail); - this.rich_thumbnail = item.richThumbnail && ResultsParser.parseItem(item.richThumbnail); - this.title = new Text(item.title, ''); - this.badges = Array.isArray(item.badges) ? ResultsParser.parse(item.badges) : []; - const lengthAlt = item.thumbnailOverlays.find(overlay => overlay.hasOwnProperty('thumbnailOverlayTimeStatusRenderer'))?.thumbnailOverlayTimeStatusRenderer; - this.duration = item.lengthText ? new Text(item.lengthText, '') : lengthAlt?.text ? new Text(lengthAlt.text) : ''; - this.published_at = new Text(item.publishedTimeText, ''); - this.views = new Text(item.viewCountText, ''); - // TODO: rich thumbnail? - this.endpoint = new NavigationEndpoint(item.navigationEndpoint); - } + constructor(item) { + this.id = item.videoId; + this.thumbnails = Thumbnail.fromResponse(item.thumbnail); + this.rich_thumbnail = item.richThumbnail && ResultsParser.parseItem(item.richThumbnail); + this.title = new Text(item.title, ''); + this.badges = Array.isArray(item.badges) ? ResultsParser.parse(item.badges) : []; + const lengthAlt = item.thumbnailOverlays.find(overlay => overlay.hasOwnProperty('thumbnailOverlayTimeStatusRenderer'))?.thumbnailOverlayTimeStatusRenderer; + this.duration = item.lengthText ? new Text(item.lengthText, '') : lengthAlt?.text ? new Text(lengthAlt.text) : ''; + this.published_at = new Text(item.publishedTimeText, ''); + this.views = new Text(item.viewCountText, ''); + // TODO: rich thumbnail? + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + } } module.exports = GridVideo; \ No newline at end of file diff --git a/lib/parser/contents/HorizontalCardList.js b/lib/parser/contents/HorizontalCardList.js index 31daee505..845629fdf 100644 --- a/lib/parser/contents/HorizontalCardList.js +++ b/lib/parser/contents/HorizontalCardList.js @@ -1,13 +1,13 @@ -const ResultsParser = require("."); -const Text = require("./Text"); +const ResultsParser = require('.'); +const Text = require('./Text'); class HorizontalCardList { - type = 'HorizontalCardList'; + type = 'HorizontalCardList'; - constructor(item) { - this.cards = ResultsParser.parse(item.cards); - this.header = item.header?.title ? new Text(item.header) : null; - } + constructor(item) { + this.cards = ResultsParser.parse(item.cards); + this.header = item.header?.title ? new Text(item.header) : null; + } } module.exports = HorizontalCardList; \ No newline at end of file diff --git a/lib/parser/contents/HorizontalList.js b/lib/parser/contents/HorizontalList.js index 1b27e1a7c..9b3e17766 100644 --- a/lib/parser/contents/HorizontalList.js +++ b/lib/parser/contents/HorizontalList.js @@ -1,12 +1,12 @@ -const ResultsParser = require("."); +const ResultsParser = require('.'); class HorizontalList { - type = 'HorizontalList'; + type = 'HorizontalList'; - constructor(item) { - this.visible_item_count = item.visibleItemCount; - this.items = ResultsParser.parse(item.items); - } + constructor(item) { + this.visible_item_count = item.visibleItemCount; + this.items = ResultsParser.parse(item.items); + } } module.exports = HorizontalList; \ No newline at end of file diff --git a/lib/parser/contents/Menu.js b/lib/parser/contents/Menu.js index ffabee81d..3a9a8dc8e 100644 --- a/lib/parser/contents/Menu.js +++ b/lib/parser/contents/Menu.js @@ -1,12 +1,12 @@ -const ResultsParser = require("."); +const ResultsParser = require('.'); class Menu { - type = 'Menu'; + type = 'Menu'; - constructor(item) { - this.top_level_buttons = item.topLevelButtons && ResultsParser.parse(item.topLevelButtons); - this.items = ResultsParser.parse(item.items); - } + constructor(item) { + this.top_level_buttons = item.topLevelButtons && ResultsParser.parse(item.topLevelButtons); + this.items = ResultsParser.parse(item.items); + } } module.exports = Menu; \ No newline at end of file diff --git a/lib/parser/contents/MenuNavigationItem.js b/lib/parser/contents/MenuNavigationItem.js index be3b013d2..3287043d9 100644 --- a/lib/parser/contents/MenuNavigationItem.js +++ b/lib/parser/contents/MenuNavigationItem.js @@ -1,14 +1,14 @@ -const ResultsParser = require("."); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); +const ResultsParser = require('.'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); class MenuNavigationItem { - type = 'MenuNavigationItem'; + type = 'MenuNavigationItem'; - constructor(item) { - this.endpoint = new NavigationEndpoint(item.navigationEndpoint); - this.text = new Text(item.text); - } + constructor(item) { + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + this.text = new Text(item.text); + } } module.exports = MenuNavigationItem; \ No newline at end of file diff --git a/lib/parser/contents/MenuServiceItem.js b/lib/parser/contents/MenuServiceItem.js index 9242bb46c..2fa1dca89 100644 --- a/lib/parser/contents/MenuServiceItem.js +++ b/lib/parser/contents/MenuServiceItem.js @@ -1,15 +1,15 @@ -const ResultsParser = require("."); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); +const ResultsParser = require('.'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); class MenuServiceItem { - type = 'MenuServiceItem'; + type = 'MenuServiceItem'; - constructor(item) { - this.endpoint = new NavigationEndpoint(item.serviceEndpoint); - this.text = new Text(item.text); - // TODO: icons? - } + constructor(item) { + this.endpoint = new NavigationEndpoint(item.serviceEndpoint); + this.text = new Text(item.text); + // TODO: icons? + } } module.exports = MenuServiceItem; \ No newline at end of file diff --git a/lib/parser/contents/MetadataBadge.js b/lib/parser/contents/MetadataBadge.js index ed2216337..24651f53d 100644 --- a/lib/parser/contents/MetadataBadge.js +++ b/lib/parser/contents/MetadataBadge.js @@ -1,16 +1,16 @@ -const ResultsParser = require("."); +const ResultsParser = require('.'); class MetadataBadge { - type = 'MetadataBadge'; - style; - label; - non_abbreviated_label; + type = 'MetadataBadge'; + style; + label; + non_abbreviated_label; - constructor(item) { - this.style = item.style; - this.label = item.label; - this.non_abbreviated_label = item.accessibilityData?.label; - } + constructor(item) { + this.style = item.style; + this.label = item.label; + this.non_abbreviated_label = item.accessibilityData?.label; + } } module.exports = MetadataBadge; \ No newline at end of file diff --git a/lib/parser/contents/MetadataRow.js b/lib/parser/contents/MetadataRow.js index 8e32187bf..ccd6a8811 100644 --- a/lib/parser/contents/MetadataRow.js +++ b/lib/parser/contents/MetadataRow.js @@ -1,13 +1,13 @@ -const ResultsParser = require("."); -const Text = require("./Text"); +const ResultsParser = require('.'); +const Text = require('./Text'); class MetadataRow { - type = 'MetadataRow'; + type = 'MetadataRow'; - constructor(item) { - this.contents = new Text(item.contents); - this.title = new Text(item.title); - } + constructor(item) { + this.contents = new Text(item.contents); + this.title = new Text(item.title); + } } module.exports = MetadataRow; \ No newline at end of file diff --git a/lib/parser/contents/MetadataRowContainer.js b/lib/parser/contents/MetadataRowContainer.js index 14093321f..7bfb2074b 100644 --- a/lib/parser/contents/MetadataRowContainer.js +++ b/lib/parser/contents/MetadataRowContainer.js @@ -1,11 +1,11 @@ -const ResultsParser = require("."); +const ResultsParser = require('.'); class MetadataRowContainer { - type = 'MetadataRowContainer'; + type = 'MetadataRowContainer'; - constructor(item) { - this.rows = ResultsParser.parse(item.rows); - } + constructor(item) { + this.rows = ResultsParser.parse(item.rows); + } } module.exports = MetadataRowContainer; \ No newline at end of file diff --git a/lib/parser/contents/MetadataRowHeader.js b/lib/parser/contents/MetadataRowHeader.js index afeeada73..25a2292ad 100644 --- a/lib/parser/contents/MetadataRowHeader.js +++ b/lib/parser/contents/MetadataRowHeader.js @@ -1,12 +1,12 @@ -const ResultsParser = require("."); -const Text = require("./Text"); +const ResultsParser = require('.'); +const Text = require('./Text'); class MetadataRowHeader { - type = 'MetadataRowHeader'; + type = 'MetadataRowHeader'; - constructor(item) { - this.text = new Text(item.content); - } + constructor(item) { + this.text = new Text(item.content); + } } module.exports = MetadataRowHeader; \ No newline at end of file diff --git a/lib/parser/contents/Mix.js b/lib/parser/contents/Mix.js index b1e70b905..dd7065530 100644 --- a/lib/parser/contents/Mix.js +++ b/lib/parser/contents/Mix.js @@ -1,15 +1,15 @@ -const ResultsParser = require("."); -const Playlist = require("./Playlist"); -const Thumbnail = require("./Thumbnail"); +const ResultsParser = require('.'); +const Playlist = require('./Playlist'); +const Thumbnail = require('./Thumbnail'); class Mix extends Playlist { - type = 'Mix'; + type = 'Mix'; - constructor(item) { - super(item); - delete this.thumbnails; - this.thumbnail = Thumbnail.fromResponse(item.thumbnail); - } + constructor(item) { + super(item); + delete this.thumbnails; + this.thumbnail = Thumbnail.fromResponse(item.thumbnail); + } } module.exports = Mix; \ No newline at end of file diff --git a/lib/parser/contents/MovingThumbnail.js b/lib/parser/contents/MovingThumbnail.js index 2a3dc26e5..61b7e9769 100644 --- a/lib/parser/contents/MovingThumbnail.js +++ b/lib/parser/contents/MovingThumbnail.js @@ -1,12 +1,12 @@ -const ResultsParser = require("."); -const Thumbnail = require("./Thumbnail"); +const ResultsParser = require('.'); +const Thumbnail = require('./Thumbnail'); class MovingThumbnail { - type = 'MovingThumbnail'; + type = 'MovingThumbnail'; - constructor(item) { - this.thumbnails = item.movingThumbnailDetails && Thumbnail.fromResponse(item.movingThumbnailDetails.thumbnails); - } + constructor(item) { + this.thumbnails = item.movingThumbnailDetails && Thumbnail.fromResponse(item.movingThumbnailDetails.thumbnails); + } } module.exports = MovingThumbnail; \ No newline at end of file diff --git a/lib/parser/contents/NavigatableText.js b/lib/parser/contents/NavigatableText.js index f2bf03446..c6f9f7e53 100644 --- a/lib/parser/contents/NavigatableText.js +++ b/lib/parser/contents/NavigatableText.js @@ -1,28 +1,28 @@ -const Text = require("./Text"); -const NavigationEndpoint = require("./NavigationEndpoint"); +const Text = require('./Text'); +const NavigationEndpoint = require('./NavigationEndpoint'); class NavigatableText extends Text { - type = 'NavigatableText'; - endpoint; - constructor(node) { - super(node); - // TODO: is this needed? Text now supports this itself - this.endpoint = + type = 'NavigatableText'; + endpoint; + constructor(node) { + super(node); + // TODO: is this needed? Text now supports this itself + this.endpoint = node.runs?.[0]?.navigationEndpoint ? - new NavigationEndpoint(node.runs[0].navigationEndpoint) : - node.navigationEndpoint ? + new NavigationEndpoint(node.runs[0].navigationEndpoint) : + node.navigationEndpoint ? new NavigationEndpoint(node.navigationEndpoint) : - node.titleNavigationEndpoint ? - new NavigationEndpoint(node.titleNavigationEndpoint) : null; - } + node.titleNavigationEndpoint ? + new NavigationEndpoint(node.titleNavigationEndpoint) : null; + } - toString() { - return `[${this.text}](${this.url?.toString()})`; - } + toString() { + return `[${this.text}](${this.url?.toString()})`; + } - toJSON() { - return this; - } + toJSON() { + return this; + } } module.exports = NavigatableText; diff --git a/lib/parser/contents/NavigationEndpoint.js b/lib/parser/contents/NavigationEndpoint.js index d168fa382..eb143df94 100644 --- a/lib/parser/contents/NavigationEndpoint.js +++ b/lib/parser/contents/NavigationEndpoint.js @@ -1,82 +1,82 @@ -const ResultsParser = require("."); -const { InnertubeError } = require("../../utils/Utils"); -const Actions = require("../../core/Actions"); +const ResultsParser = require('.'); +const { InnertubeError } = require('../../utils/Utils'); +const Actions = require('../../core/Actions'); class NavigationEndpoint { - type = 'NavigationEndpoint'; + type = 'NavigationEndpoint'; - constructor (item) { - this.metadata = { - api_url: item.commandMetadata.webCommandMetadata.api_url, - url: item.commandMetadata.webCommandMetadata.url, - send_post: item.commandMetadata.webCommandMetadata.sendPost, - page_type: item.commandMetadata.webCommandMetadata.webPageType, - } - // TODO: clean this up! - this.browse = item.browseEndpoint ? { - browseId: item.browseEndpoint.browseId, - params: item.browseEndpoint.params, - base_url: item.browseEndpoint.canonicalBaseUrl - } : null; - // this is the video id to navigate to - this.watchVideo = item.watchEndpoint ? { - video_id: item.watchEndpoint.videoId, - playlist_id: item.watchEndpoint.playlistId, - index: item.watchEndpoint.index, // this is the video index in the playlist - params: item.watchEndpoint.params, - } : null; - // this is a search button - this.search = item.searchEndpoint ? { - query: item.searchEndpoint.query, - params: item.searchEndpoint.params || null, - } : null; - // this is a playlist page to navigate to - // but redirect and actually start playing it - // see url for index (playnext and index searchParams) - this.watchPlaylist = item.watchPlaylistEndpoint?.playlistId; - // reels have their own navigation endpoint for some reason - this.watchReel = item.reelWatchEndpoint ? { - video_id: item.reelWatchEndpoint.videoId, - player_params: item.reelWatchEndpoint.playerParams, - params: item.reelWatchEndpoint.params, - sequence_provider: item.reelWatchEndpoint.sequenceProvider, - sequence_params: item.reelWatchEndpoint.sequenceParams, - } : null; - // external url - this.url = item.urlEndpoint ? { - url: new URL(item.urlEndpoint.url), - target: item.urlEndpoint.target, - nofollow: item.urlEndpoint.nofollow || false - } : null; - // continuations - this.continuation = item.continuationCommand ? { - request: item.continuationCommand.request, // 'CONTINUATION_REQUEST_TYPE_BROWSE' -> filter the browse results on home_feed? - token: item.continuationCommand.token, - trigger: item.trigger // TODO: is this the right place for this? - } : null; - this.is_reveal_business_emal = !!item.revealBusinessEmailCommand; - // TODO: sign in endpoints - - // TODO: modal endpoints cleanup - const modalRenderer = item.modalEndpoint?.moadlWithTitleAndButtonRenderer; - this.modal = modalRenderer ? { - title: modalRenderer.title, - button: ResultsParser.parseItem(modalRenderer.button), - content: modalRenderer.content, - } : null; + constructor (item) { + this.metadata = { + api_url: item.commandMetadata.webCommandMetadata.api_url, + url: item.commandMetadata.webCommandMetadata.url, + send_post: item.commandMetadata.webCommandMetadata.sendPost, + page_type: item.commandMetadata.webCommandMetadata.webPageType, } + // TODO: clean this up! + this.browse = item.browseEndpoint ? { + browseId: item.browseEndpoint.browseId, + params: item.browseEndpoint.params, + base_url: item.browseEndpoint.canonicalBaseUrl + } : null; + // this is the video id to navigate to + this.watchVideo = item.watchEndpoint ? { + video_id: item.watchEndpoint.videoId, + playlist_id: item.watchEndpoint.playlistId, + index: item.watchEndpoint.index, // this is the video index in the playlist + params: item.watchEndpoint.params, + } : null; + // this is a search button + this.search = item.searchEndpoint ? { + query: item.searchEndpoint.query, + params: item.searchEndpoint.params || null, + } : null; + // this is a playlist page to navigate to + // but redirect and actually start playing it + // see url for index (playnext and index searchParams) + this.watchPlaylist = item.watchPlaylistEndpoint?.playlistId; + // reels have their own navigation endpoint for some reason + this.watchReel = item.reelWatchEndpoint ? { + video_id: item.reelWatchEndpoint.videoId, + player_params: item.reelWatchEndpoint.playerParams, + params: item.reelWatchEndpoint.params, + sequence_provider: item.reelWatchEndpoint.sequenceProvider, + sequence_params: item.reelWatchEndpoint.sequenceParams, + } : null; + // external url + this.url = item.urlEndpoint ? { + url: new URL(item.urlEndpoint.url), + target: item.urlEndpoint.target, + nofollow: item.urlEndpoint.nofollow || false + } : null; + // continuations + this.continuation = item.continuationCommand ? { + request: item.continuationCommand.request, // 'CONTINUATION_REQUEST_TYPE_BROWSE' -> filter the browse results on home_feed? + token: item.continuationCommand.token, + trigger: item.trigger // TODO: is this the right place for this? + } : null; + this.is_reveal_business_emal = !!item.revealBusinessEmailCommand; + // TODO: sign in endpoints + + // TODO: modal endpoints cleanup + const modalRenderer = item.modalEndpoint?.moadlWithTitleAndButtonRenderer; + this.modal = modalRenderer ? { + title: modalRenderer.title, + button: ResultsParser.parseItem(modalRenderer.button), + content: modalRenderer.content, + } : null; + } - call(session) { - if (this.continuation) { - switch (this.continuation.request) { - case 'CONTINUATION_REQUEST_TYPE_BROWSE': - return session.actions.browse(this.continuation.token, { is_ctoken: true }); + call(session) { + if (this.continuation) { + switch (this.continuation.request) { + case 'CONTINUATION_REQUEST_TYPE_BROWSE': + return session.actions.browse(this.continuation.token, { is_ctoken: true }); - default: - throw new InnertubeError(`Unknown continuation request type: ${this.continuation.request}`); - } - } + default: + throw new InnertubeError(`Unknown continuation request type: ${this.continuation.request}`); + } } + } } module.exports = NavigationEndpoint; \ No newline at end of file diff --git a/lib/parser/contents/Playlist.js b/lib/parser/contents/Playlist.js index 34e25daf7..0d32073e2 100644 --- a/lib/parser/contents/Playlist.js +++ b/lib/parser/contents/Playlist.js @@ -1,23 +1,23 @@ -const ResultsParser = require("."); -const Author = require("./Author"); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); -const Thumbnail = require("./Thumbnail"); +const ResultsParser = require('.'); +const Author = require('./Author'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); +const Thumbnail = require('./Thumbnail'); class Playlist { - type = 'Playlist'; + type = 'Playlist'; - constructor(item) { - this.id = item.playlistId; - this.title = new Text(item.title); - this.author = item.longBylineText.simpleText ? null : new Author(item.longBylineText, item.ownerBadges); - this.thumbnails = Array.isArray(item.thumbnails) ? item.thumbnails.map(thumbs => Thumbnail.fromResponse(thumbs)) : []; - if (new Text(item.videoCountText) !== 'Mix') - this.videos = parseInt(item.videoCount); - this.first_videos = ResultsParser.parse(item.videos); - this.endpoint = new NavigationEndpoint(item.navigationEndpoint); - this.view_playlist = new Text(item.viewPlaylistText); - } + constructor(item) { + this.id = item.playlistId; + this.title = new Text(item.title); + this.author = item.longBylineText.simpleText ? null : new Author(item.longBylineText, item.ownerBadges); + this.thumbnails = Array.isArray(item.thumbnails) ? item.thumbnails.map(thumbs => Thumbnail.fromResponse(thumbs)) : []; + if (new Text(item.videoCountText) !== 'Mix') + this.videos = parseInt(item.videoCount); + this.first_videos = ResultsParser.parse(item.videos); + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + this.view_playlist = new Text(item.viewPlaylistText); + } } module.exports = Playlist; \ No newline at end of file diff --git a/lib/parser/contents/PlaylistPanelVideo.js b/lib/parser/contents/PlaylistPanelVideo.js index fada0a0bb..add8c941b 100644 --- a/lib/parser/contents/PlaylistPanelVideo.js +++ b/lib/parser/contents/PlaylistPanelVideo.js @@ -1,22 +1,22 @@ -const ResultsParser = require("."); -const Author = require("./Author"); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); -const Thumbnail = require("./Thumbnail"); +const ResultsParser = require('.'); +const Author = require('./Author'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); +const Thumbnail = require('./Thumbnail'); class PlaylistPanelVideo { - type = 'PlaylistPanelVideo'; + type = 'PlaylistPanelVideo'; - constructor(item) { - this.index = new Text(item.indexText).text; - this.selected = item.selected; - this.duration = new Text(item.lengthText); - this.author = new Author(item.longBylineText); - this.endpoint = new NavigationEndpoint(item.navigationEndpoint); - this.thumbnails = Thumbnail.fromResponse(item.thumbnail); - this.title = new Text(item.title); - this.id = item.videoId; - } + constructor(item) { + this.index = new Text(item.indexText).text; + this.selected = item.selected; + this.duration = new Text(item.lengthText); + this.author = new Author(item.longBylineText); + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + this.thumbnails = Thumbnail.fromResponse(item.thumbnail); + this.title = new Text(item.title); + this.id = item.videoId; + } } module.exports = PlaylistPanelVideo; \ No newline at end of file diff --git a/lib/parser/contents/PlaylistVideo.js b/lib/parser/contents/PlaylistVideo.js index 34997a726..921f28a32 100644 --- a/lib/parser/contents/PlaylistVideo.js +++ b/lib/parser/contents/PlaylistVideo.js @@ -1,22 +1,22 @@ -const ResultsParser = require("."); -const NavigatableText = require("./NavigatableText"); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); -const Thumbnail = require("./Thumbnail"); +const ResultsParser = require('.'); +const NavigatableText = require('./NavigatableText'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); +const Thumbnail = require('./Thumbnail'); class PlaylistVideo { - type = 'PlaylistVideo'; + type = 'PlaylistVideo'; - constructor(item) { - this.index = new Text(item.index).text; - this.is_playable = item.isPlayable; - this.duration = new Text(item.lengthText); - this.endpoint = new NavigationEndpoint(item.navigationEndpoint); - this.author = new NavigatableText(item.shortBylineText); - this.thumbnails = Thumbnail.fromResponse(item.thumbnail); - this.title = new Text(item.title); - this.id = item.videoId; - } + constructor(item) { + this.index = new Text(item.index).text; + this.is_playable = item.isPlayable; + this.duration = new Text(item.lengthText); + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + this.author = new NavigatableText(item.shortBylineText); + this.thumbnails = Thumbnail.fromResponse(item.thumbnail); + this.title = new Text(item.title); + this.id = item.videoId; + } } module.exports = PlaylistVideo; \ No newline at end of file diff --git a/lib/parser/contents/PlaylistVideoList.js b/lib/parser/contents/PlaylistVideoList.js index 2be4587aa..8ce06c717 100644 --- a/lib/parser/contents/PlaylistVideoList.js +++ b/lib/parser/contents/PlaylistVideoList.js @@ -1,14 +1,14 @@ -const ResultsParser = require("."); +const ResultsParser = require('.'); class PlaylistVideoList { - type = 'PlaylistVideoList'; + type = 'PlaylistVideoList'; - constructor(item) { - this.is_editable = item.isEditable; - this.can_reorder = item.canReorder; - this.id = item.playlistId; - this.contents = ResultsParser.parse(item.contents); - } + constructor(item) { + this.is_editable = item.isEditable; + this.can_reorder = item.canReorder; + this.id = item.playlistId; + this.contents = ResultsParser.parse(item.contents); + } } module.exports = PlaylistVideoList; \ No newline at end of file diff --git a/lib/parser/contents/ReelItem.js b/lib/parser/contents/ReelItem.js index cdd07d94c..1c5d77521 100644 --- a/lib/parser/contents/ReelItem.js +++ b/lib/parser/contents/ReelItem.js @@ -1,18 +1,18 @@ -const ResultsParser = require("."); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); -const Thumbnail = require("./Thumbnail"); +const ResultsParser = require('.'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); +const Thumbnail = require('./Thumbnail'); class ReelItem { - type = 'ReelItem'; + type = 'ReelItem'; - constructor(item) { - this.id = item.videoId; - this.title = new Text(item.headline, ''); - this.thumbnails = Thumbnail.fromResponse(item.thumbnail); - this.views = new Text(item.viewCountText, ''); - this.endpoint = new NavigationEndpoint(item.navigationEndpoint); - } + constructor(item) { + this.id = item.videoId; + this.title = new Text(item.headline, ''); + this.thumbnails = Thumbnail.fromResponse(item.thumbnail); + this.views = new Text(item.viewCountText, ''); + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + } } module.exports = ReelItem; \ No newline at end of file diff --git a/lib/parser/contents/ReelShelf.js b/lib/parser/contents/ReelShelf.js index fd1c9f861..cb7544b50 100644 --- a/lib/parser/contents/ReelShelf.js +++ b/lib/parser/contents/ReelShelf.js @@ -1,15 +1,15 @@ -const ResultsParser = require("."); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); +const ResultsParser = require('.'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); class ReelShelf { - type = 'ReelShelf'; + type = 'ReelShelf'; - constructor(item) { - this.title = new Text(item.title); - this.items = ResultsParser.parse(item.items); - this.endpoint = item.endpoint ? new NavigationEndpoint(item.endpoint) : null; - } + constructor(item) { + this.title = new Text(item.title); + this.items = ResultsParser.parse(item.items); + this.endpoint = item.endpoint ? new NavigationEndpoint(item.endpoint) : null; + } } module.exports = ReelShelf; \ No newline at end of file diff --git a/lib/parser/contents/RichGrid.js b/lib/parser/contents/RichGrid.js index 180fb41e8..50ccf5c3d 100644 --- a/lib/parser/contents/RichGrid.js +++ b/lib/parser/contents/RichGrid.js @@ -1,14 +1,14 @@ -const ResultsParser = require("."); +const ResultsParser = require('.'); class RichGrid { - type = 'RichGrid'; + type = 'RichGrid'; - constructor(item) { - // XXX: we don't parse the masthead since it is usually an advertisement - // XXX: reflowOptions aren't parsed, I think its only used internally for layout - this.header = ResultsParser.parseItem(item.header); - this.contents = ResultsParser.parse(item.contents); - } + constructor(item) { + // XXX: we don't parse the masthead since it is usually an advertisement + // XXX: reflowOptions aren't parsed, I think its only used internally for layout + this.header = ResultsParser.parseItem(item.header); + this.contents = ResultsParser.parse(item.contents); + } } module.exports = RichGrid; \ No newline at end of file diff --git a/lib/parser/contents/RichShelf.js b/lib/parser/contents/RichShelf.js index c9ef9eee7..f34d36791 100644 --- a/lib/parser/contents/RichShelf.js +++ b/lib/parser/contents/RichShelf.js @@ -1,15 +1,15 @@ -const ResultsParser = require("."); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); +const ResultsParser = require('.'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); class RichShelf { - type = 'RichShelf'; + type = 'RichShelf'; - constructor(item) { - this.title = new Text(item.title); - this.contents = ResultsParser.parse(item.contents); - this.endpoint = item.endpoint ? new NavigationEndpoint(item.endpoint) : null; - } + constructor(item) { + this.title = new Text(item.title); + this.contents = ResultsParser.parse(item.contents); + this.endpoint = item.endpoint ? new NavigationEndpoint(item.endpoint) : null; + } } module.exports = RichShelf; \ No newline at end of file diff --git a/lib/parser/contents/SearchRefinementCard.js b/lib/parser/contents/SearchRefinementCard.js index 21d0b1196..18c60716b 100644 --- a/lib/parser/contents/SearchRefinementCard.js +++ b/lib/parser/contents/SearchRefinementCard.js @@ -1,18 +1,18 @@ -const ResultsParser = require("."); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); -const Thumbnail = require("./Thumbnail"); +const ResultsParser = require('.'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); +const Thumbnail = require('./Thumbnail'); class SearchRefinementCard { - type = 'SearchRefinementCard'; + type = 'SearchRefinementCard'; - constructor(item) { - this.thumbnail = Thumbnail.fromResponse(item.thumbnail); - this.endpoint = new NavigationEndpoint(item.searchEndpoint); - this.query = new Text(item.query); - // XXX: is this actually useful? - this.style = item.searchRefinementCardRendererStyle?.value; - } + constructor(item) { + this.thumbnail = Thumbnail.fromResponse(item.thumbnail); + this.endpoint = new NavigationEndpoint(item.searchEndpoint); + this.query = new Text(item.query); + // XXX: is this actually useful? + this.style = item.searchRefinementCardRendererStyle?.value; + } } module.exports = SearchRefinementCard; \ No newline at end of file diff --git a/lib/parser/contents/Shelf.js b/lib/parser/contents/Shelf.js index 7e0c1ef2e..bbd725eaf 100644 --- a/lib/parser/contents/Shelf.js +++ b/lib/parser/contents/Shelf.js @@ -1,22 +1,22 @@ -const ResultsParser = require("."); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); +const ResultsParser = require('.'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); class Shelf { - type = 'Shelf'; + type = 'Shelf'; - constructor(item) { - this.title = new Text(item.title); - this.content = ResultsParser.parseItem(item.content); - this.endpoint = item.endpoint ? new NavigationEndpoint(item.endpoint) : null; - // XXX: maybe add this as buttonRenderer? - // this is the playAllButton in the original response - this.button = item.playAllButton?.buttonRenderer ? { - text: new Text(item.playAllButton.buttonRenderer.text, ''), - endpoint: item.playAllButton.buttonRenderer.navigationEndpoint ? new NavigationEndpoint(item.playAllButton.buttonRenderer.navigationEndpoint) : null, - icon: item.playAllButton.buttonRenderer.icon?.iconType || 'UNKNOWN' - } : null; - } + constructor(item) { + this.title = new Text(item.title); + this.content = ResultsParser.parseItem(item.content); + this.endpoint = item.endpoint ? new NavigationEndpoint(item.endpoint) : null; + // XXX: maybe add this as buttonRenderer? + // this is the playAllButton in the original response + this.button = item.playAllButton?.buttonRenderer ? { + text: new Text(item.playAllButton.buttonRenderer.text, ''), + endpoint: item.playAllButton.buttonRenderer.navigationEndpoint ? new NavigationEndpoint(item.playAllButton.buttonRenderer.navigationEndpoint) : null, + icon: item.playAllButton.buttonRenderer.icon?.iconType || 'UNKNOWN' + } : null; + } } module.exports = Shelf; \ No newline at end of file diff --git a/lib/parser/contents/Tab.js b/lib/parser/contents/Tab.js index f5d1efe03..ce32cc726 100644 --- a/lib/parser/contents/Tab.js +++ b/lib/parser/contents/Tab.js @@ -1,15 +1,15 @@ -const ResultsParser = require("."); -const NavigationEndpoint = require("./NavigationEndpoint"); +const ResultsParser = require('.'); +const NavigationEndpoint = require('./NavigationEndpoint'); class Tab { - type = 'Tab'; + type = 'Tab'; - constructor(item) { - this.title = item.title; - this.endpoint = item.endpoint ? new NavigationEndpoint(item.endpoint) : null; - this.selected = item.selected; // if this.selected then we may have content else we do not - this.content = item.content ? ResultsParser.parseItem(item.content) : null; - } + constructor(item) { + this.title = item.title; + this.endpoint = item.endpoint ? new NavigationEndpoint(item.endpoint) : null; + this.selected = item.selected; // if this.selected then we may have content else we do not + this.content = item.content ? ResultsParser.parseItem(item.content) : null; + } } module.exports = Tab; \ No newline at end of file diff --git a/lib/parser/contents/Text.js b/lib/parser/contents/Text.js index ffb1ea45f..ace1800da 100644 --- a/lib/parser/contents/Text.js +++ b/lib/parser/contents/Text.js @@ -1,35 +1,35 @@ -const TextRun = require("./TextRun"); +const TextRun = require('./TextRun'); class Text { - type = 'Text'; - text; - constructor(txt, def = null) { - if (typeof txt !== 'object') { - this.text = def; - } - else if (txt.hasOwnProperty('simpleText')) - this.text = txt.simpleText; - else if (Array.isArray(txt.runs)) { - this.text = txt.runs.map(a => a.text).join(''); - this.runs = txt.runs.map(a => new TextRun(a)); - } - else this.text = def; + type = 'Text'; + text; + constructor(txt, def = null) { + if (typeof txt !== 'object') { + this.text = def; } - - toString() { - return this.text; + else if (txt.hasOwnProperty('simpleText')) + this.text = txt.simpleText; + else if (Array.isArray(txt.runs)) { + this.text = txt.runs.map(a => a.text).join(''); + this.runs = txt.runs.map(a => new TextRun(a)); } + else this.text = def; + } + + toString() { + return this.text; + } - toJSON() { - return { - text: this.text, - runs: this.runs || [ - { - text: this.text - } - ] - }; - } + toJSON() { + return { + text: this.text, + runs: this.runs || [ + { + text: this.text + } + ] + }; + } } module.exports = Text; \ No newline at end of file diff --git a/lib/parser/contents/TextRun.js b/lib/parser/contents/TextRun.js index 76fee75bd..6358e96bd 100644 --- a/lib/parser/contents/TextRun.js +++ b/lib/parser/contents/TextRun.js @@ -1,14 +1,14 @@ -const ResultsParser = require("."); -const NavigationEndpoint = require("./NavigationEndpoint"); +const ResultsParser = require('.'); +const NavigationEndpoint = require('./NavigationEndpoint'); class TextRun { - type = 'TextRun'; - text; - endpoint; - constructor(node) { - this.text = node.text; - this.endpoint = node.navigationEndpoint && new NavigationEndpoint(node.navigationEndpoint); - } + type = 'TextRun'; + text; + endpoint; + constructor(node) { + this.text = node.text; + this.endpoint = node.navigationEndpoint && new NavigationEndpoint(node.navigationEndpoint); + } } module.exports = TextRun; \ No newline at end of file diff --git a/lib/parser/contents/Thumbnail.js b/lib/parser/contents/Thumbnail.js index fe8f8b2e5..add0e67fe 100644 --- a/lib/parser/contents/Thumbnail.js +++ b/lib/parser/contents/Thumbnail.js @@ -1,33 +1,33 @@ class Thumbnail { - /** + /** * @type {string} */ - url; - /** + url; + /** * @type {number} */ - width; - /** + width; + /** * @type {number} */ - height; - constructor ({ url, width, height }) { - this.url = url; - this.width = width; - this.height = height; - } + height; + constructor ({ url, width, height }) { + this.url = url; + this.width = width; + this.height = height; + } - /** + /** * Get thumbnails from response object * @param {*} response response object * @returns {Thumbnail[]} sorted array of thumbnails */ - static fromResponse({ thumbnails }) { - if (!thumbnails) { - return; - } - return thumbnails.map(x => new Thumbnail(x)).sort((a, b) => b.width - a.width); + static fromResponse({ thumbnails }) { + if (!thumbnails) { + return; } + return thumbnails.map(x => new Thumbnail(x)).sort((a, b) => b.width - a.width); + } } module.exports = Thumbnail; \ No newline at end of file diff --git a/lib/parser/contents/ToggleButton.js b/lib/parser/contents/ToggleButton.js index d90d8f75e..c6276e2f6 100644 --- a/lib/parser/contents/ToggleButton.js +++ b/lib/parser/contents/ToggleButton.js @@ -1,48 +1,48 @@ -const ResultsParser = require("."); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); +const ResultsParser = require('.'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); class ToggleButton { - type = 'ToggleButton'; + type = 'ToggleButton'; - constructor(item) { - this.is_toggled = item.isToggled; - this.is_disabled = item.isDisabled; - this.default_service_endpoint = item.defaultServiceEndpoint && new NavigationEndpoint(item.defaultServiceEndpoint); - this.toggled_service_endpoint = item.toggledServiceEndpoint && new NavigationEndpoint(item.toggledServiceEndpoint); - this.default_navigation_endpoint = item.defaultNavigationEndpoint && new NavigationEndpoint(item.defaultNavigationEndpoint); - this.default_tooltip = item.defaultTooltip; - this.toggled_tooltip = item.toggledTooltip; - this.default_text = new Text(item.defaultText); - this.toggled_text = new Text(item.toggledText); - } + constructor(item) { + this.is_toggled = item.isToggled; + this.is_disabled = item.isDisabled; + this.default_service_endpoint = item.defaultServiceEndpoint && new NavigationEndpoint(item.defaultServiceEndpoint); + this.toggled_service_endpoint = item.toggledServiceEndpoint && new NavigationEndpoint(item.toggledServiceEndpoint); + this.default_navigation_endpoint = item.defaultNavigationEndpoint && new NavigationEndpoint(item.defaultNavigationEndpoint); + this.default_tooltip = item.defaultTooltip; + this.toggled_tooltip = item.toggledTooltip; + this.default_text = new Text(item.defaultText); + this.toggled_text = new Text(item.toggledText); + } - get endpoint() { - if (this.default_navigation_endpoint) { - return this.default_navigation_endpoint; - } - if (this.is_toggled && this.toggled_service_endpoint) { - return this.toggled_service_endpoint; - } - if (!this.is_toggled && this.default_service_endpoint) { - return this.default_service_endpoint; - } - return null; + get endpoint() { + if (this.default_navigation_endpoint) { + return this.default_navigation_endpoint; + } + if (this.is_toggled && this.toggled_service_endpoint) { + return this.toggled_service_endpoint; + } + if (!this.is_toggled && this.default_service_endpoint) { + return this.default_service_endpoint; } + return null; + } - get tooltip() { - if (this.is_toggled) { - return this.toggled_tooltip; - } - return this.default_tooltip; + get tooltip() { + if (this.is_toggled) { + return this.toggled_tooltip; } + return this.default_tooltip; + } - get text() { - if (this.is_toggled) { - return this.toggled_text; - } - return this.default_text; + get text() { + if (this.is_toggled) { + return this.toggled_text; } + return this.default_text; + } } module.exports = ToggleButton; \ No newline at end of file diff --git a/lib/parser/contents/TwoColumnBrowseResults.js b/lib/parser/contents/TwoColumnBrowseResults.js index 8f5fd19f5..03b93771a 100644 --- a/lib/parser/contents/TwoColumnBrowseResults.js +++ b/lib/parser/contents/TwoColumnBrowseResults.js @@ -1,11 +1,11 @@ -const ResultsParser = require("."); +const ResultsParser = require('.'); class TwoColumnBrowseResults { - type = 'TwoColumnBrowseResults'; + type = 'TwoColumnBrowseResults'; - constructor(item) { - this.tabs = ResultsParser.parse(item.tabs); - } + constructor(item) { + this.tabs = ResultsParser.parse(item.tabs); + } } module.exports = TwoColumnBrowseResults; \ No newline at end of file diff --git a/lib/parser/contents/TwoColumnSearchResults.js b/lib/parser/contents/TwoColumnSearchResults.js index 49b818fa4..a63b8ffd6 100644 --- a/lib/parser/contents/TwoColumnSearchResults.js +++ b/lib/parser/contents/TwoColumnSearchResults.js @@ -1,13 +1,13 @@ -const ResultsParser = require("."); +const ResultsParser = require('.'); class TwoColumnSearchResults { - type = 'TwoColumnSearchResults'; + type = 'TwoColumnSearchResults'; - constructor(items) { - this.primary = ResultsParser.parseItem(items.primaryContents); - if (items.secondaryContents) - this.secondary = ResultsParser.parseItem(items.secondaryContents); - } + constructor(items) { + this.primary = ResultsParser.parseItem(items.primaryContents); + if (items.secondaryContents) + this.secondary = ResultsParser.parseItem(items.secondaryContents); + } } module.exports = TwoColumnSearchResults; \ No newline at end of file diff --git a/lib/parser/contents/TwoColumnWatchNextResults.js b/lib/parser/contents/TwoColumnWatchNextResults.js index 5cb550ae1..0727e6c18 100644 --- a/lib/parser/contents/TwoColumnWatchNextResults.js +++ b/lib/parser/contents/TwoColumnWatchNextResults.js @@ -1,34 +1,34 @@ -const ResultsParser = require("."); -const Author = require("./Author"); -const NavigatableText = require("./NavigatableText"); -const NavigationEndpoint = require("./NavigationEndpoint"); +const ResultsParser = require('.'); +const Author = require('./Author'); +const NavigatableText = require('./NavigatableText'); +const NavigationEndpoint = require('./NavigationEndpoint'); class TwoColumnWatchNextResult { - type = 'TwoColumnWatchNextResult'; + type = 'TwoColumnWatchNextResult'; - constructor(item) { - // XXX: do we need this? - // this.autoplay - // this contains the video info - this.primary = ResultsParser.parse(item.results.results.contents); - // these hold the recommendations - this.secondary = ResultsParser.parse(item.secondaryResults.secondaryResults.results); - // playlist data - const playlist = item.playlist?.playlist; - this.playlist = playlist && { - current_index: playlist.currentIndex, - endpoint: new NavigationEndpoint(playlist.endpoint), - is_course: playlist.isCourse, - is_infinite: playlist.isInfinite, - author: new Author(playlist.longBylineText, playlist.ownerBadges), - save: playlist.saveButton && ResultsParser.parseItem(playlist.saveButton), - title: new NavigatableText(playlist.titleText), - videos: playlist.totalVideos, - contents: ResultsParser.parse(playlist.contents), - } - // TODO: conversationBar - // this.conversation = liveChatRenderer + constructor(item) { + // XXX: do we need this? + // this.autoplay + // this contains the video info + this.primary = ResultsParser.parse(item.results.results.contents); + // these hold the recommendations + this.secondary = ResultsParser.parse(item.secondaryResults.secondaryResults.results); + // playlist data + const playlist = item.playlist?.playlist; + this.playlist = playlist && { + current_index: playlist.currentIndex, + endpoint: new NavigationEndpoint(playlist.endpoint), + is_course: playlist.isCourse, + is_infinite: playlist.isInfinite, + author: new Author(playlist.longBylineText, playlist.ownerBadges), + save: playlist.saveButton && ResultsParser.parseItem(playlist.saveButton), + title: new NavigatableText(playlist.titleText), + videos: playlist.totalVideos, + contents: ResultsParser.parse(playlist.contents), } + // TODO: conversationBar + // this.conversation = liveChatRenderer + } } module.exports = TwoColumnWatchNextResult; \ No newline at end of file diff --git a/lib/parser/contents/UniversalWatchCard.js b/lib/parser/contents/UniversalWatchCard.js index 79f02718b..3887eb883 100644 --- a/lib/parser/contents/UniversalWatchCard.js +++ b/lib/parser/contents/UniversalWatchCard.js @@ -1,13 +1,13 @@ -const ResultsParser = require("."); +const ResultsParser = require('.'); class UniversalWatchCard { - type = 'UniversalWatchCard'; + type = 'UniversalWatchCard'; - constructor(items) { - this.header = ResultsParser.parseItem(items.header); - this.hero = ResultsParser.parseItem(items.callToAction); - this.sections = ResultsParser.parse(items.sections); - } + constructor(items) { + this.header = ResultsParser.parseItem(items.header); + this.hero = ResultsParser.parseItem(items.callToAction); + this.sections = ResultsParser.parse(items.sections); + } } module.exports = UniversalWatchCard; \ No newline at end of file diff --git a/lib/parser/contents/VerticalList.js b/lib/parser/contents/VerticalList.js index 13c0855ac..fb94e6c2b 100644 --- a/lib/parser/contents/VerticalList.js +++ b/lib/parser/contents/VerticalList.js @@ -1,12 +1,12 @@ -const ResultsParser = require("."); +const ResultsParser = require('.'); class VerticalList { - type = 'VerticalList'; + type = 'VerticalList'; - constructor(item) { - this.collapsed_item_count = item.collapsedItemCount; - this.items = ResultsParser.parse(item.items); - } + constructor(item) { + this.collapsed_item_count = item.collapsedItemCount; + this.items = ResultsParser.parse(item.items); + } } module.exports = VerticalList; \ No newline at end of file diff --git a/lib/parser/contents/Video.js b/lib/parser/contents/Video.js index a6c4fa3d3..3f8127c4d 100644 --- a/lib/parser/contents/Video.js +++ b/lib/parser/contents/Video.js @@ -1,8 +1,8 @@ -const ResultsParser = require("."); -const Author = require("./Author"); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); -const Thumbnail = require("./Thumbnail"); +const ResultsParser = require('.'); +const Author = require('./Author'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); +const Thumbnail = require('./Thumbnail'); class Video { type = 'Video'; diff --git a/lib/parser/contents/VideoOwner.js b/lib/parser/contents/VideoOwner.js index 0bb7b0be8..c5d3d0221 100644 --- a/lib/parser/contents/VideoOwner.js +++ b/lib/parser/contents/VideoOwner.js @@ -1,16 +1,16 @@ -const ResultsParser = require("."); -const Author = require("./Author"); +const ResultsParser = require('.'); +const Author = require('./Author'); class VideoOwner { - type = 'VideoOwner'; + type = 'VideoOwner'; - constructor(item) { - /* + constructor(item) { + /* this.author = new Author({ ...item.title, navigationEndpoint: item.navigationEndpoint }, item.badges, item.thumbnail); */ - } + } } module.exports = VideoOwner; \ No newline at end of file diff --git a/lib/parser/contents/VideoPrimaryInfo.js b/lib/parser/contents/VideoPrimaryInfo.js index a73d70ef4..9e19b5128 100644 --- a/lib/parser/contents/VideoPrimaryInfo.js +++ b/lib/parser/contents/VideoPrimaryInfo.js @@ -1,16 +1,16 @@ -const ResultsParser = require("."); -const Text = require("./Text"); +const ResultsParser = require('.'); +const Text = require('./Text'); class VideoPrimaryInfo { - type = 'VideoPrimaryInfo'; + type = 'VideoPrimaryInfo'; - constructor(item) { - this.title = new Text(item.title); - this.published_at = new Date(item.dateText); - // menuRenderer - this.actions = ResultsParser.parseItem(item.videoActions); - this.views = new Text(item.viewCount.videoViewCountRenderer?.viewCount || item.viewCount.videoViewCountRenderer?.shortViewCount); - } + constructor(item) { + this.title = new Text(item.title); + this.published_at = new Date(item.dateText); + // menuRenderer + this.actions = ResultsParser.parseItem(item.videoActions); + this.views = new Text(item.viewCount.videoViewCountRenderer?.viewCount || item.viewCount.videoViewCountRenderer?.shortViewCount); + } } module.exports = VideoPrimaryInfo; \ No newline at end of file diff --git a/lib/parser/contents/VideoSecondaryInfo.js b/lib/parser/contents/VideoSecondaryInfo.js index 79556d6e1..9ed2784b3 100644 --- a/lib/parser/contents/VideoSecondaryInfo.js +++ b/lib/parser/contents/VideoSecondaryInfo.js @@ -1,15 +1,15 @@ -const ResultsParser = require("."); -const Text = require("./Text"); +const ResultsParser = require('.'); +const Text = require('./Text'); class VideoSecondaryInfo { - type = 'VideoSecondaryInfo'; + type = 'VideoSecondaryInfo'; - constructor(item) { - this.owner = ResultsParser.parseItem(item.owner); - this.description = new Text(item.description); - this.metadata = ResultsParser.parseItem(item.metadataRowContainer); - this.description_collapsed_lines = item.descriptionCollapsedLines; - } + constructor(item) { + this.owner = ResultsParser.parseItem(item.owner); + this.description = new Text(item.description); + this.metadata = ResultsParser.parseItem(item.metadataRowContainer); + this.description_collapsed_lines = item.descriptionCollapsedLines; + } } module.exports = VideoSecondaryInfo; \ No newline at end of file diff --git a/lib/parser/contents/WatchCardCompactVideo.js b/lib/parser/contents/WatchCardCompactVideo.js index be53ce183..76057c1c3 100644 --- a/lib/parser/contents/WatchCardCompactVideo.js +++ b/lib/parser/contents/WatchCardCompactVideo.js @@ -1,21 +1,21 @@ -const ResultsParser = require("."); -const NavigatableText = require("./NavigatableText"); -const NavigationEndpoint = require("./NavigationEndpoint"); -const Text = require("./Text"); +const ResultsParser = require('.'); +const NavigatableText = require('./NavigatableText'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); class WatchCardCompactVideo { - type = 'WatchCardCompactVideo'; + type = 'WatchCardCompactVideo'; - constructor(item) { - this.title = new Text(item.title); - const [ views, publishedAt ] = new Text(item.subtitle).split('•'); - this.views = views.trim(); - this.published_at = publishedAt?.trim(); - this.endpoint = new NavigationEndpoint(item.navigationEndpoint); - this.duration = new Text(item.lengthText); - // TODO: byline is author? - this.byline = new NavigatableText(item.byline); - } + constructor(item) { + this.title = new Text(item.title); + const [ views, publishedAt ] = new Text(item.subtitle).split('•'); + this.views = views.trim(); + this.published_at = publishedAt?.trim(); + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + this.duration = new Text(item.lengthText); + // TODO: byline is author? + this.byline = new NavigatableText(item.byline); + } } module.exports = WatchCardCompactVideo; \ No newline at end of file diff --git a/lib/parser/contents/WatchCardHeroVideo.js b/lib/parser/contents/WatchCardHeroVideo.js index f2046b77b..5cb421b72 100644 --- a/lib/parser/contents/WatchCardHeroVideo.js +++ b/lib/parser/contents/WatchCardHeroVideo.js @@ -1,13 +1,13 @@ -const ResultsParser = require("."); -const NavigationEndpoint = require("./NavigationEndpoint"); +const ResultsParser = require('.'); +const NavigationEndpoint = require('./NavigationEndpoint'); class WatchCardHeroVideo { - type = 'WatchCardHeroVideo'; + type = 'WatchCardHeroVideo'; - constructor(item) { - this.endpoint = new NavigationEndpoint(item.navigationEndpoint); - this.collage = ResultsParser.parseItem(item.heroImage); - } + constructor(item) { + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + this.collage = ResultsParser.parseItem(item.heroImage); + } } module.exports = WatchCardHeroVideo; \ No newline at end of file diff --git a/lib/parser/contents/WatchCardRichHeader.js b/lib/parser/contents/WatchCardRichHeader.js index e0c7e1ebf..668df9d14 100644 --- a/lib/parser/contents/WatchCardRichHeader.js +++ b/lib/parser/contents/WatchCardRichHeader.js @@ -1,16 +1,16 @@ -const ResultsParser = require("."); -const Author = require("./Author"); -const Text = require("./Text"); +const ResultsParser = require('.'); +const Author = require('./Author'); +const Text = require('./Text'); class WatchCardRichHeader { - type = 'WatchCardRichHeader'; + type = 'WatchCardRichHeader'; - constructor(item) { - this.title = new Text(item.title); - this.subtitle = new Text(item.subtitle); - this.author = new Author(item, [item.titleBadge], item.avatar); - this.author.name = this.title; - } + constructor(item) { + this.title = new Text(item.title); + this.subtitle = new Text(item.subtitle); + this.author = new Author(item, [item.titleBadge], item.avatar); + this.author.name = this.title; + } } module.exports = WatchCardRichHeader; \ No newline at end of file diff --git a/lib/parser/contents/WatchCardSectionSequence.js b/lib/parser/contents/WatchCardSectionSequence.js index dd6503399..ed75695f8 100644 --- a/lib/parser/contents/WatchCardSectionSequence.js +++ b/lib/parser/contents/WatchCardSectionSequence.js @@ -1,12 +1,12 @@ -const ResultsParser = require("."); -const Text = require("./Text"); +const ResultsParser = require('.'); +const Text = require('./Text'); class WatchCardSectionSequence { - type = 'WatchCardSectionSequence'; + type = 'WatchCardSectionSequence'; - constructor(item) { - this.lists = ResultsParser.parse(item.lists); - } + constructor(item) { + this.lists = ResultsParser.parse(item.lists); + } } module.exports = WatchCardSectionSequence; \ No newline at end of file diff --git a/lib/parser/contents/index.js b/lib/parser/contents/index.js index dda0e5023..40a70d8f2 100644 --- a/lib/parser/contents/index.js +++ b/lib/parser/contents/index.js @@ -1,108 +1,108 @@ class AppendContinuationItemsAction { - type = 'AppendContinuationItemsAction'; + type = 'AppendContinuationItemsAction'; - constructor (item) { - this.continuation_items = ResultsParser.parse(item.continuationItems); - } + constructor (item) { + this.continuation_items = ResultsParser.parse(item.continuationItems); + } } class ResultsParser { - static parseResponse(data) { - return { - contents: data.contents && ResultsParser.parseItem(data.contents), - on_response_received_actions: data.onResponseReceivedActions && ResultsParser.parseRRA(data.onResponseReceivedActions) || undefined, - } - } - - static parseRRA(actions) { - return actions.map((action) => { - if (Object.keys(action).includes('appendContinuationItemsAction')) - return new AppendContinuationItemsAction(action.appendContinuationItemsAction); - }).filter((item) => item); + static parseResponse(data) { + return { + contents: data.contents && ResultsParser.parseItem(data.contents), + on_response_received_actions: data.onResponseReceivedActions && ResultsParser.parseRRA(data.onResponseReceivedActions) || undefined, } + } - static parse(contents) { - return contents.map((item) => this.parseItem(item)).filter((item) => item); - } + static parseRRA(actions) { + return actions.map((action) => { + if (Object.keys(action).includes('appendContinuationItemsAction')) + return new AppendContinuationItemsAction(action.appendContinuationItemsAction); + }).filter((item) => item); + } - static parseItem(item) { - const renderers = { - twoColumnSearchResultsRenderer: require('./TwoColumnSearchResults'), - twoColumnBrowseResultsRenderer: require('./TwoColumnBrowseResults'), - tabRenderer: require('./Tab'), - expandableTabRenderer: require('./ExpandableTab'), - videoRenderer: require('./Video'), - metadataBadgeRenderer: require('./MetadataBadge'), - channelRenderer: require('./Channel'), - playlistRenderer: require('./Playlist'), - childVideoRenderer: require('./ChildVideo'), - radioRenderer: require('./Mix'), - shelfRenderer: require('./Shelf'), - verticalListRenderer: require('./VerticalList'), - horizontalListRenderer: require('./HorizontalList'), - sectionListRenderer: require('./GenericList')('SectionList'), - secondarySearchContainerRenderer: require('./GenericList')('SecondarySearchContainer'), - itemSectionRenderer: require('./GenericList')('ItemSection'), - universalWatchCardRenderer: require('./UniversalWatchCard'), - watchCardRichHeaderRenderer: require('./WatchCardRichHeader'), - watchCardHeroVideoRenderer: require('./WatchCardHeroVideo'), - collageHeroImageRenderer: require('./CollageHeroImage'), - watchCardSectionSequenceRenderer: require('./WatchCardSectionSequence'), - verticalWatchCardListRenderer: require('./GenericList')('VerticalWatchCardList', 'items'), - horizontalCardListRenderer: require('./HorizontalCardList'), - watchCardCompactVideoRenderer: require('./WatchCardCompactVideo'), - searchRefinementCardRenderer: require('./SearchRefinementCard'), - channelVideoPlayerRenderer: require('./ChannelVideoPlayer'), - gridVideoRenderer: require('./GridVideo'), - gridChannelRenderer: require('./GridChannel'), - reelShelfRenderer: require('./ReelShelf'), - reelItemRenderer: require('./ReelItem'), - gridRenderer: require('./GenericList')('Grid', 'items'), - gridPlaylistRenderer: require('./GridPlaylist'), - backstagePostThreadRenderer: require('./BackstagePostThread'), - backstagePostRenderer: require('./BackstagePost'), - backstageImageRenderer: require('./BackstageImage'), - commentActionButtonsRenderer: require('./CommentActionButtons'), - buttonRenderer: require('./Button'), - toggleButtonRenderer: require('./ToggleButton'), - continuationItemRenderer: require('./ContinuationItem'), - channelAboutFullMetadataRenderer: require('./ChannelAboutFullMetadata'), - playlistVideoListRenderer: require('./PlaylistVideoList'), - playlistVideoRenderer: require('./PlaylistVideo'), - richGridRenderer: require('./RichGrid'), - feedFilterChipBarRenderer: require('./GenericList')('FeedFilterChipBar'), - chipCloudChipRenderer: require('./ChipCloudChip'), - richItemRenderer: require('./GenericContainer')('RichItem'), - richShelfRenderer: require('./RichShelf'), - richSectionRenderer: require('./GenericContainer')('RichSection'), - twoColumnWatchNextResults: require('./TwoColumnWatchNextResults'), - videoPrimaryInfoRenderer: require('./VideoPrimaryInfo'), - menuRenderer: require('./Menu'), - menuNavigationItemRenderer: require('./MenuNavigationItem'), - menuServiceItemRenderer: require('./MenuServiceItem'), - videoSecondaryInfoRenderer: require('./VideoSecondaryInfo'), - videoOwnerRenderer: require('./VideoOwner'), - metadataRowContainerRenderer: require('./MetadataRowContainer'), - metadataRowHeaderRenderer: require('./MetadataRowHeader'), - metadataRowRenderer: require('./MetadataRow'), - compactVideoRenderer: require('./CompactVideo'), - playlistPanelVideoRenderer: require('./PlaylistPanelVideo'), - movingThumbnailRenderer: require('./MovingThumbnail'), - } + static parse(contents) { + return contents.map((item) => this.parseItem(item)).filter((item) => item); + } - const keys = Reflect.ownKeys(item); - if (keys.length !== 1) return null; + static parseItem(item) { + const renderers = { + twoColumnSearchResultsRenderer: require('./TwoColumnSearchResults'), + twoColumnBrowseResultsRenderer: require('./TwoColumnBrowseResults'), + tabRenderer: require('./Tab'), + expandableTabRenderer: require('./ExpandableTab'), + videoRenderer: require('./Video'), + metadataBadgeRenderer: require('./MetadataBadge'), + channelRenderer: require('./Channel'), + playlistRenderer: require('./Playlist'), + childVideoRenderer: require('./ChildVideo'), + radioRenderer: require('./Mix'), + shelfRenderer: require('./Shelf'), + verticalListRenderer: require('./VerticalList'), + horizontalListRenderer: require('./HorizontalList'), + sectionListRenderer: require('./GenericList')('SectionList'), + secondarySearchContainerRenderer: require('./GenericList')('SecondarySearchContainer'), + itemSectionRenderer: require('./GenericList')('ItemSection'), + universalWatchCardRenderer: require('./UniversalWatchCard'), + watchCardRichHeaderRenderer: require('./WatchCardRichHeader'), + watchCardHeroVideoRenderer: require('./WatchCardHeroVideo'), + collageHeroImageRenderer: require('./CollageHeroImage'), + watchCardSectionSequenceRenderer: require('./WatchCardSectionSequence'), + verticalWatchCardListRenderer: require('./GenericList')('VerticalWatchCardList', 'items'), + horizontalCardListRenderer: require('./HorizontalCardList'), + watchCardCompactVideoRenderer: require('./WatchCardCompactVideo'), + searchRefinementCardRenderer: require('./SearchRefinementCard'), + channelVideoPlayerRenderer: require('./ChannelVideoPlayer'), + gridVideoRenderer: require('./GridVideo'), + gridChannelRenderer: require('./GridChannel'), + reelShelfRenderer: require('./ReelShelf'), + reelItemRenderer: require('./ReelItem'), + gridRenderer: require('./GenericList')('Grid', 'items'), + gridPlaylistRenderer: require('./GridPlaylist'), + backstagePostThreadRenderer: require('./BackstagePostThread'), + backstagePostRenderer: require('./BackstagePost'), + backstageImageRenderer: require('./BackstageImage'), + commentActionButtonsRenderer: require('./CommentActionButtons'), + buttonRenderer: require('./Button'), + toggleButtonRenderer: require('./ToggleButton'), + continuationItemRenderer: require('./ContinuationItem'), + channelAboutFullMetadataRenderer: require('./ChannelAboutFullMetadata'), + playlistVideoListRenderer: require('./PlaylistVideoList'), + playlistVideoRenderer: require('./PlaylistVideo'), + richGridRenderer: require('./RichGrid'), + feedFilterChipBarRenderer: require('./GenericList')('FeedFilterChipBar'), + chipCloudChipRenderer: require('./ChipCloudChip'), + richItemRenderer: require('./GenericContainer')('RichItem'), + richShelfRenderer: require('./RichShelf'), + richSectionRenderer: require('./GenericContainer')('RichSection'), + twoColumnWatchNextResults: require('./TwoColumnWatchNextResults'), + videoPrimaryInfoRenderer: require('./VideoPrimaryInfo'), + menuRenderer: require('./Menu'), + menuNavigationItemRenderer: require('./MenuNavigationItem'), + menuServiceItemRenderer: require('./MenuServiceItem'), + videoSecondaryInfoRenderer: require('./VideoSecondaryInfo'), + videoOwnerRenderer: require('./VideoOwner'), + metadataRowContainerRenderer: require('./MetadataRowContainer'), + metadataRowHeaderRenderer: require('./MetadataRowHeader'), + metadataRowRenderer: require('./MetadataRow'), + compactVideoRenderer: require('./CompactVideo'), + playlistPanelVideoRenderer: require('./PlaylistPanelVideo'), + movingThumbnailRenderer: require('./MovingThumbnail'), + } - if (!renderers.hasOwnProperty(keys[0])) { - console.warn('No renderer found for type: ', keys[0]); - return null; - } + const keys = Reflect.ownKeys(item); + if (keys.length !== 1) return null; - const result = new renderers[keys[0]](item[keys[0]]); - if (keys[0] === 'gridChannelRenderer') - console.log(result); - return result; + if (!renderers.hasOwnProperty(keys[0])) { + console.warn('No renderer found for type: ', keys[0]); + return null; } + + const result = new renderers[keys[0]](item[keys[0]]); + if (keys[0] === 'gridChannelRenderer') + console.log(result); + return result; + } } module.exports = ResultsParser; \ No newline at end of file From 87d7bf83cc340a0505c6f5d4c70c1830753f1e68 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sun, 1 May 2022 14:58:46 +0200 Subject: [PATCH 18/55] feat: Trending API --- lib/Innertube.js | 13 +-- lib/core/HomeFeed.js | 47 +------- lib/core/SimpleVideo.js | 72 +++++++++++++ lib/core/Trending.js | 126 ++++++++++++++++++++++ lib/parser/contents/Author.js | 4 + lib/parser/contents/CompactVideo.js | 4 + lib/parser/contents/GridVideo.js | 8 +- lib/parser/contents/MetadataBadge.js | 8 ++ lib/parser/contents/NavigationEndpoint.js | 9 ++ lib/parser/contents/PlaylistPanelVideo.js | 4 + lib/parser/contents/PlaylistVideo.js | 8 +- lib/parser/contents/Tab.js | 5 + lib/parser/contents/Text.js | 14 ++- lib/parser/contents/WatchCardHeroVideo.js | 1 + lib/parser/contents/index.js | 1 + 15 files changed, 266 insertions(+), 58 deletions(-) create mode 100644 lib/core/SimpleVideo.js create mode 100644 lib/core/Trending.js diff --git a/lib/Innertube.js b/lib/Innertube.js index e2e3b1ee6..68916e062 100644 --- a/lib/Innertube.js +++ b/lib/Innertube.js @@ -18,8 +18,8 @@ const Constants = require('./utils/Constants'); const Proto = require('./proto'); const NToken = require('./deciphers/NToken'); const Signature = require('./deciphers/Signature'); -const ResultsParser = require('./parser/contents'); const HomeFeed = require('./core/HomeFeed'); +const { Trending } = require('./core/Trending'); class Innertube { #oauth; @@ -572,20 +572,13 @@ class Innertube { /** * Retrieves trending content. * - * @returns {Promise.<{ now: { content: [{ title: string; videos: []; }] }; - * music: { getVideos: Promise.; }; gaming: { getVideos: Promise.; }; - * gaming: { getVideos: Promise.; }; }>} + * @returns {Promise} */ async getTrending() { const response = await this.actions.browse('FEtrending'); if (!response.success) throw new Utils.InnertubeError('Could not retrieve trending content', response); - const trending = new Parser(this, response, { - client: 'YOUTUBE', - data_type: 'TRENDING' - }).parse(); - - return trending; + return new Trending(this, response.data); } async getLibrary() { diff --git a/lib/core/HomeFeed.js b/lib/core/HomeFeed.js index ca0a3197e..45f4d4421 100644 --- a/lib/core/HomeFeed.js +++ b/lib/core/HomeFeed.js @@ -2,6 +2,7 @@ const ResultsParser = require("../parser/contents"); const Simplify = require("../parser/simplify"); const { InnertubeError } = require("../utils/Utils"); const Utils = require("../utils/Utils"); +const SimpleVideo = require("./SimpleVideo"); class HomeFeed { #page; @@ -31,11 +32,11 @@ class HomeFeed { /** * Get all the videos in the home feed - * @returns {Array} + * @returns {Array} */ getVideos() { return Simplify.matching({ - type: Simplify.matching(/^Video$/), + type: Simplify.matching(SimpleVideo.regex), }).runOn(this.#page); } @@ -65,50 +66,12 @@ class HomeFeed { /** * Get all the videos in the home feed - * @deprecated Use getVideos instead + * @returns {SimpleVideo[]} */ get videos() { const simplified = this.getVideos(); - return simplified.map(video => { - return { - id: video.id, - title: video.title.toString(), - description: video.description, - channel: { - id: video.author.id, - name: video.author.name, - url: video.author.endpoint.metadata.url, - thumbnail: video.author.best_thumbnail, - is_verified: video.author.is_verified, - is_verified_artist: video.author.is_verified_artist, - }, - metadata: { - view_count: video.views.toString(), - short_view_count_text: { - simple_text: video.short_view_count.toString(), - // TODO: the accessiblity text is not yet parsed - accessibility_label: "", - }, - thumbnail: video.best_thumbnail, - thumbnails: video.thumbnails, - // XXX: It doesn't look like this is returned? - // but I'll send it anyway - moving_thumbnail: video.rich_thumbnail?.thumbnails?.[0] || {}, - moving_thumbnails: video.rich_thumbnail?.thumbnails || [], - published: video.published_at.toString(), - duration: { - seconds: Utils.timeToSeconds(video.duration.toString()), - simple_text: video.duration.toString(), - // TODO: again - we need access to the accessibility data here - accessibility_label: "", - }, - // XXX: this is different return types from the existing API - badges: video.badges, - owner_badges: video.author.badges, - } - } - }); + return simplified.map(video => new SimpleVideo(video)); } async getContinuation() { diff --git a/lib/core/SimpleVideo.js b/lib/core/SimpleVideo.js new file mode 100644 index 000000000..9312a87f8 --- /dev/null +++ b/lib/core/SimpleVideo.js @@ -0,0 +1,72 @@ +const Utils = require('../utils/Utils'); + +/** + * Wraps around: + * - Video + * - GridVideo + * - CompactVideo + * - PlaylistVideo + * - PlaylistPanelVideo + * - TODO: WatchCardCompactVideo + * + * Provides a unified interface for all of them. + */ +class SimpleVideo { + /** + * @type {import('../parser/contents/Video') | import('../parser/contents/GridVideo') | import('../parser/contents/CompactVideo') | import('../parser/contents/PlaylistVideo') | import('../parser/contents/PlaylistPanelVideo')} + */ + #wrapper; + constructor(video) { + this.#wrapper = video; + } + + getUnderlyingRenderer() { + return this.#wrapper; + } + + get id() { + return this.#wrapper.id; + } + + get title() { + return this.#wrapper.title.toString(); + } + + get description() { + return this.#wrapper.description && this.#wrapper.description.toString() || ''; + } + + get channel() { + return this.#wrapper.author; + } + + get metadata() { + return { + view_count: this.#wrapper.views?.toString(), + short_view_count_text: this.#wrapper.short_view_count && { + simple_text: this.#wrapper.short_view_count.toString(), + // TODO: the accessiblity text is not yet parsed + accessibility_label: "", + }, + thumbnail: this.#wrapper.best_thumbnail, + thumbnails: this.#wrapper.thumbnails, + moving_thumbnail: this.#wrapper.rich_thumbnail?.thumbnails?.[0] || {}, + moving_thumbnails: this.#wrapper.rich_thumbnail?.thumbnails || [], + published: this.#wrapper.published_at?.toString(), + duration: { + seconds: Utils.timeToSeconds(this.#wrapper.duration.toString()), + simple_text: this.#wrapper.duration.toString(), + // TODO: again - we need access to the accessibility data here + accessibility_label: "", + }, + badges: this.#wrapper.badges?.map(badge => badge.toString()) || [], + owner_badges: this.#wrapper.author?.badges?.map(badge => badge.toString()), + } + } + + static get regex() { + return /^(Video|GridVideo|CompactVideo|PlaylistVideo|PlaylistPanelVideo)$/; + } +} + +module.exports = SimpleVideo; \ No newline at end of file diff --git a/lib/core/Trending.js b/lib/core/Trending.js new file mode 100644 index 000000000..62829e594 --- /dev/null +++ b/lib/core/Trending.js @@ -0,0 +1,126 @@ +const ResultsParser = require("../parser/contents"); +const Simplify = require("../parser/simplify"); +const { InnertubeError } = require("../utils/Utils"); +const SimpleVideo = require("./SimpleVideo"); + +class TrendingTab { + /** + * @type {import('../parser/contents/Tab')} + */ + #tab; + #session; + /** + * @param {import('../parser/contents/Tab')} tab + */ + constructor(session, tab) { + this.#tab = tab; + this.#session = session; + } + + get title() { + return this.#tab.title; + } + + /** + * @returns {import('../parser/contents/Shelf')[]} + */ + getShelfs() { + return Simplify.matching({ + type: Simplify.matching(/^Shelf$/), + }).runOn(this.#tab.content); + } + + getShelf(title) { + return this.getAllShelves().find(shelf => shelf.title.toString() === title); + } + + /** + * @type {{ + * title: string, + * videos: SimpleVideo[] + * }[] | null} + */ + get content() { + if (!this.#tab.content) return null; + const shelfs = this.getShelfs(); + const ret = shelfs.map(shelf => { + return { + title: shelf.title.toString(), + videos: Simplify.matching({ + type: Simplify.matching(SimpleVideo.regex), + }).runOn(shelf.content).map(video => new SimpleVideo(video)), + } + }); + return ret; + } + + /** + * Selects this tab and returns a new TrendingTab with this tab selected + */ + async getSelected() { + if (this.#tab.selected) return this; + const response = await this.#tab.endpoint.call(this.#session); + if (!response.success) throw new InnertubeError('Failed to get selected tab', response); + return new Trending(this.#session, response.data).getTab(this.title); + } + + /** + * @note getVideos returns only the vidoes of the first shelf + * @returns {SimpleVideo[]} + */ + async getVideos() { + if (this.#tab.selected) return this.content[0]?.videos; + return (await this.getSelected()).content[0]?.videos; + } + + get raw() { + return this.#tab; + } +} + +class Trending { + #page; + /** + * @type {import('../parser/contents/Tab')[]} + */ + #tabs; + #session; + constructor (session, data) { + this.#session = session; + this.#page = ResultsParser.parseResponse(data); + this.#tabs = Simplify.matching({ + type: Simplify.matching(/^Tab$/), + }).runOn(this.#page); + } + + /** + * + * @param {string} title title of the tab to get + * @returns + */ + getTab(title) { + const tab = this.#tabs.find(tab => tab.title.toLowerCase() === title.toLowerCase()); + if (!tab) throw new InnertubeError(`Tab ${name} not found`); + return new TrendingTab(this.#session, tab); + } + + /** + * @alias getTab('now') + */ + get now() { + return this.getTab('now'); + } + + /** + * @alias getTab('music') + */ + get music() { + return this.getTab('music'); + } + + get page() { + return this.#page; + } +} + +module.exports = { Trending, TrendingTab }; \ No newline at end of file diff --git a/lib/parser/contents/Author.js b/lib/parser/contents/Author.js index 071b950ec..9fc79f815 100644 --- a/lib/parser/contents/Author.js +++ b/lib/parser/contents/Author.js @@ -23,6 +23,10 @@ class Author { } } + get url() { + return this.#nav_text.endpoint.metadata.url; + } + get name() { return this.#nav_text.toString(); } diff --git a/lib/parser/contents/CompactVideo.js b/lib/parser/contents/CompactVideo.js index e02797eeb..750234749 100644 --- a/lib/parser/contents/CompactVideo.js +++ b/lib/parser/contents/CompactVideo.js @@ -18,6 +18,10 @@ class CompactVideo { this.duration = new Text(item.lengthText); this.endpoint = new NavigationEndpoint(item.navigationEndpoint); } + + get best_thumbnail() { + return this.thumbnails[0]; + } } module.exports = CompactVideo; \ No newline at end of file diff --git a/lib/parser/contents/GridVideo.js b/lib/parser/contents/GridVideo.js index 9cb1fbbbc..2679737c3 100644 --- a/lib/parser/contents/GridVideo.js +++ b/lib/parser/contents/GridVideo.js @@ -1,4 +1,5 @@ const ResultsParser = require('.'); +const Author = require('./Author'); const NavigationEndpoint = require('./NavigationEndpoint'); const Text = require('./Text'); const Thumbnail = require('./Thumbnail'); @@ -8,6 +9,7 @@ class GridVideo { constructor(item) { this.id = item.videoId; + this.author = new Author(item.shortBylineText, item.ownerBadges); this.thumbnails = Thumbnail.fromResponse(item.thumbnail); this.rich_thumbnail = item.richThumbnail && ResultsParser.parseItem(item.richThumbnail); this.title = new Text(item.title, ''); @@ -16,8 +18,12 @@ class GridVideo { this.duration = item.lengthText ? new Text(item.lengthText, '') : lengthAlt?.text ? new Text(lengthAlt.text) : ''; this.published_at = new Text(item.publishedTimeText, ''); this.views = new Text(item.viewCountText, ''); - // TODO: rich thumbnail? this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + this.short_view_count = new Text(item.shortViewCountText, ''); + } + + get best_thumbnail() { + return this.thumbnails[0]; } } diff --git a/lib/parser/contents/MetadataBadge.js b/lib/parser/contents/MetadataBadge.js index 24651f53d..5ee7fac93 100644 --- a/lib/parser/contents/MetadataBadge.js +++ b/lib/parser/contents/MetadataBadge.js @@ -11,6 +11,14 @@ class MetadataBadge { this.label = item.label; this.non_abbreviated_label = item.accessibilityData?.label; } + + /** + * Get label as string + * @returns {string} + */ + toString() { + return this.non_abbreviated_label || this.label; + } } module.exports = MetadataBadge; \ No newline at end of file diff --git a/lib/parser/contents/NavigationEndpoint.js b/lib/parser/contents/NavigationEndpoint.js index eb143df94..37660953d 100644 --- a/lib/parser/contents/NavigationEndpoint.js +++ b/lib/parser/contents/NavigationEndpoint.js @@ -66,6 +66,11 @@ class NavigationEndpoint { } : null; } + /** + * + * @param {import('../../Innertube')} session + * @returns + */ call(session) { if (this.continuation) { switch (this.continuation.request) { @@ -76,6 +81,10 @@ class NavigationEndpoint { throw new InnertubeError(`Unknown continuation request type: ${this.continuation.request}`); } } + + if (this.browse) { + return session.actions.browse(this.browse.browseId, { params: this.browse.params }); + } } } diff --git a/lib/parser/contents/PlaylistPanelVideo.js b/lib/parser/contents/PlaylistPanelVideo.js index add8c941b..bb5ed6dfc 100644 --- a/lib/parser/contents/PlaylistPanelVideo.js +++ b/lib/parser/contents/PlaylistPanelVideo.js @@ -17,6 +17,10 @@ class PlaylistPanelVideo { this.title = new Text(item.title); this.id = item.videoId; } + + get best_thumbnail() { + return this.thumbnails[0]; + } } module.exports = PlaylistPanelVideo; \ No newline at end of file diff --git a/lib/parser/contents/PlaylistVideo.js b/lib/parser/contents/PlaylistVideo.js index 921f28a32..cfeae59e8 100644 --- a/lib/parser/contents/PlaylistVideo.js +++ b/lib/parser/contents/PlaylistVideo.js @@ -1,5 +1,5 @@ const ResultsParser = require('.'); -const NavigatableText = require('./NavigatableText'); +const Author = require('./Author'); const NavigationEndpoint = require('./NavigationEndpoint'); const Text = require('./Text'); const Thumbnail = require('./Thumbnail'); @@ -12,11 +12,15 @@ class PlaylistVideo { this.is_playable = item.isPlayable; this.duration = new Text(item.lengthText); this.endpoint = new NavigationEndpoint(item.navigationEndpoint); - this.author = new NavigatableText(item.shortBylineText); + this.author = new Author(item.shortBylineText); this.thumbnails = Thumbnail.fromResponse(item.thumbnail); this.title = new Text(item.title); this.id = item.videoId; } + + get best_thumbnail() { + return this.thumbnails[0]; + } } module.exports = PlaylistVideo; \ No newline at end of file diff --git a/lib/parser/contents/Tab.js b/lib/parser/contents/Tab.js index ce32cc726..d0482f1e1 100644 --- a/lib/parser/contents/Tab.js +++ b/lib/parser/contents/Tab.js @@ -4,6 +4,11 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class Tab { type = 'Tab'; + /** + * @type {string} + */ + title; + constructor(item) { this.title = item.title; this.endpoint = item.endpoint ? new NavigationEndpoint(item.endpoint) : null; diff --git a/lib/parser/contents/Text.js b/lib/parser/contents/Text.js index ace1800da..9e160a89c 100644 --- a/lib/parser/contents/Text.js +++ b/lib/parser/contents/Text.js @@ -2,8 +2,11 @@ const TextRun = require('./TextRun'); class Text { type = 'Text'; + /** + * @type {string | undefined} + */ text; - constructor(txt, def = null) { + constructor(txt, def = undefined) { if (typeof txt !== 'object') { this.text = def; } @@ -16,13 +19,18 @@ class Text { else this.text = def; } + /** + * Get the string representation of this text + * @note may return an empty string if this.text is undefined + * @returns {string} + */ toString() { - return this.text; + return this.text || ''; } toJSON() { return { - text: this.text, + string: this.toString(), runs: this.runs || [ { text: this.text diff --git a/lib/parser/contents/WatchCardHeroVideo.js b/lib/parser/contents/WatchCardHeroVideo.js index 5cb421b72..cd63813b7 100644 --- a/lib/parser/contents/WatchCardHeroVideo.js +++ b/lib/parser/contents/WatchCardHeroVideo.js @@ -4,6 +4,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class WatchCardHeroVideo { type = 'WatchCardHeroVideo'; + // TODO: is this all? constructor(item) { this.endpoint = new NavigationEndpoint(item.navigationEndpoint); this.collage = ResultsParser.parseItem(item.heroImage); diff --git a/lib/parser/contents/index.js b/lib/parser/contents/index.js index 40a70d8f2..dbaee7673 100644 --- a/lib/parser/contents/index.js +++ b/lib/parser/contents/index.js @@ -88,6 +88,7 @@ class ResultsParser { compactVideoRenderer: require('./CompactVideo'), playlistPanelVideoRenderer: require('./PlaylistPanelVideo'), movingThumbnailRenderer: require('./MovingThumbnail'), + expandedShelfContentsRenderer: require('./GenericList')('ExpandedShelfContents', 'items'), } const keys = Reflect.ownKeys(item); From f72cdf5d78b26d24ddd594e638bfa20d5f4d6ec1 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sun, 1 May 2022 16:20:16 +0200 Subject: [PATCH 19/55] feat: reimplement existing channel API --- lib/Innertube.js | 10 +-- lib/core/Channel.js | 88 ++++++++++++++++++++++++++ lib/parser/contents/ChannelMetadata.js | 24 ++++--- lib/parser/contents/GridVideo.js | 2 +- lib/parser/contents/index.js | 7 +- 5 files changed, 108 insertions(+), 23 deletions(-) create mode 100644 lib/core/Channel.js diff --git a/lib/Innertube.js b/lib/Innertube.js index 68916e062..54d02528e 100644 --- a/lib/Innertube.js +++ b/lib/Innertube.js @@ -20,6 +20,7 @@ const NToken = require('./deciphers/NToken'); const Signature = require('./deciphers/Signature'); const HomeFeed = require('./core/HomeFeed'); const { Trending } = require('./core/Trending'); +const Channel = require('./core/Channel'); class Innertube { #oauth; @@ -528,18 +529,13 @@ class Innertube { * Retrieves contents for a given channel. (WIP) * * @param {string} id - channel id - * @return {Promise.<{ title: string; description: string; metadata: object; content: object }>} + * @return {Promise} */ async getChannel(id) { const response = await this.actions.browse(id); if (!response.success) throw new Utils.InnertubeError('Could not retrieve channel info.', response); - const channel_info = new Parser(this, response.data, { - client: 'YOUTUBE', - data_type: 'CHANNEL' - }).parse(); - - return channel_info; + return new Channel(this, response.data); } /** diff --git a/lib/core/Channel.js b/lib/core/Channel.js new file mode 100644 index 000000000..f60bb33a8 --- /dev/null +++ b/lib/core/Channel.js @@ -0,0 +1,88 @@ +const ResultsParser = require("../parser/contents"); +const Simplify = require("../parser/simplify"); +const SimpleVideo = require("./SimpleVideo"); + +class Channel { + #page; + #session; + constructor(session, data) { + this.#session = session; + this.#page = ResultsParser.parseResponse(data); + } + + get title() { + return this.metadata.title || ''; + } + + get description() { + return this.metadata.description || ''; + } + + /** + * @type {import('../parser/contents/ChannelMetadata')} + */ + get metadata() { + return this.#page.metadata; + } + + /** + * + * @returns {import('../parser/contents/Tab')[]} + */ + getTabs() { + return Simplify.matching({ + type: Simplify.matching(/^Tab$/), + }).runOn(this.#page); + } + + /** + * + * @param {string} name + * @returns {import('../parser/contents/Tab')} + */ + getTab(name) { + return Simplify.matching({ + type: Simplify.matching(/^Tab$/), + title: Simplify.matching(new RegExp('^' + name + '$', 'i')), + }).runOn(this.#page)[0]; + } + + #getHomePageVideos() { + const tab = this.getTab('Home'); + /** + * @type {import('../parser/contents/Shelf')[]} + */ + const shelfs = Simplify.matching({ + type: Simplify.matching(/^Shelf$/), + }).runOn(tab); + return shelfs.map(shelf => { + return { + title: shelf.title.toString(), + content: Simplify.matching({ + type: Simplify.matching(SimpleVideo.regex), + }).runOn(shelf).map(video => new SimpleVideo(video)), + } + }); + } + + /** + * @note home_page only returns videos! + * XXX: should some other API be made available to expose the content of the channel + * or should I just leave it like this and expect more advanced users + * to use the getTab() method and navigate the tabs themselves using + * the Simplify API to extract what they need? + */ + get content() { + return { + home_page: this.#getHomePageVideos(), + // TODO: + getVideos: () => {}, + getPlaylists: () => {}, + getCommunity: () => {}, + getChannels: () => {}, + getAbout: () => {} + } + } +} + +module.exports = Channel; \ No newline at end of file diff --git a/lib/parser/contents/ChannelMetadata.js b/lib/parser/contents/ChannelMetadata.js index 6633bec48..2b382751b 100644 --- a/lib/parser/contents/ChannelMetadata.js +++ b/lib/parser/contents/ChannelMetadata.js @@ -7,19 +7,17 @@ class ChannelMetadata { constructor(item) { this.title = item.title; this.description = item.description; - this.metadata = { - url: item.url, - rss_urls: item.rssUrl, - vanity_channel_url: item.vanityChannelUrl, - external_id: item.externalId, - is_family_safe: item.isFamilySafe, - keywords: item.keywords, - avatar: Thumbnail.fromResponse(item.avatar), - available_countries: item.availableCountryCodes, - android_deep_link: item.androidDeepLink, - android_appindexing_link: item.androidAppIndexingLink, - ios_appindexing_link: item.iosAppIndexingLink, - } + this.url = item.channelUrl; + this.rss_urls = item.rssUrl; + this.vanity_channel_url = item.vanityChannelUrl; + this.external_id = item.externalId; + this.is_family_safe = item.isFamilySafe; + this.keywords = item.keywords; + this.avatar = Thumbnail.fromResponse(item.avatar); + this.available_countries = item.availableCountryCodes; + this.android_deep_link = item.androidDeepLink; + this.android_appindexing_link = item.androidAppindexingLink; + this.ios_appindexing_link = item.iosAppindexingLink; } } diff --git a/lib/parser/contents/GridVideo.js b/lib/parser/contents/GridVideo.js index 2679737c3..ff8c62578 100644 --- a/lib/parser/contents/GridVideo.js +++ b/lib/parser/contents/GridVideo.js @@ -9,7 +9,7 @@ class GridVideo { constructor(item) { this.id = item.videoId; - this.author = new Author(item.shortBylineText, item.ownerBadges); + this.author = item.shortBylineText && new Author(item.shortBylineText, item.ownerBadges); this.thumbnails = Thumbnail.fromResponse(item.thumbnail); this.rich_thumbnail = item.richThumbnail && ResultsParser.parseItem(item.richThumbnail); this.title = new Text(item.title, ''); diff --git a/lib/parser/contents/index.js b/lib/parser/contents/index.js index dbaee7673..7fb42eca9 100644 --- a/lib/parser/contents/index.js +++ b/lib/parser/contents/index.js @@ -11,6 +11,10 @@ class ResultsParser { return { contents: data.contents && ResultsParser.parseItem(data.contents), on_response_received_actions: data.onResponseReceivedActions && ResultsParser.parseRRA(data.onResponseReceivedActions) || undefined, + metadata: data.metadata && ResultsParser.parseItem(data.metadata), + header: data.header && ResultsParser.parseItem(data.header), + // XXX: microformat contains meta tags for SEO? + microformat: data.microformat && ResultsParser.parseItem(data.microformat), } } @@ -89,6 +93,7 @@ class ResultsParser { playlistPanelVideoRenderer: require('./PlaylistPanelVideo'), movingThumbnailRenderer: require('./MovingThumbnail'), expandedShelfContentsRenderer: require('./GenericList')('ExpandedShelfContents', 'items'), + channelMetadataRenderer: require('./ChannelMetadata'), } const keys = Reflect.ownKeys(item); @@ -100,8 +105,6 @@ class ResultsParser { } const result = new renderers[keys[0]](item[keys[0]]); - if (keys[0] === 'gridChannelRenderer') - console.log(result); return result; } } From af73c4b1dbfd1ff13a4bd3b0bc025f655c66202e Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sun, 1 May 2022 19:13:18 +0200 Subject: [PATCH 20/55] feat: add base video feed class --- lib/core/Feed.js | 82 ++++++++++++++++++++++++++++++++++++++++++++ lib/core/HomeFeed.js | 67 +++--------------------------------- 2 files changed, 87 insertions(+), 62 deletions(-) create mode 100644 lib/core/Feed.js diff --git a/lib/core/Feed.js b/lib/core/Feed.js new file mode 100644 index 000000000..21f74b95e --- /dev/null +++ b/lib/core/Feed.js @@ -0,0 +1,82 @@ +const ResultsParser = require('../parser/contents'); +const Simplify = require('../parser/simplify'); +const SimpleVideo = require('./SimpleVideo'); + +class Feed { + #page; + /** + * @type {import('../parser/contents/ContinuationItem')[]} + */ + #continuation; + /** + * @type {import('../Innertube')} + */ + #session; + constructor(session, data) { + if (data.on_response_received_actions) + this.#page = data; + else + this.#page = ResultsParser.parseResponse(data); + this.#session = session; + } + + + /** + * Get the original page data + */ + get page() { + return this.#page; + } + + get session() { + return this.#session; + } + + /** + * Get all the videos in the feed + * @returns {Array} + */ + getVideos() { + return Simplify.matching({ + type: Simplify.matching(SimpleVideo.regex), + }).runOn(this.#page); + } + + /** + * Get unified list of videos + * @returns {SimpleVideo[]} + */ + get videos() { + const simplified = this.getVideos(); + + return simplified.map(video => new SimpleVideo(video)); + } + + 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]; + const response = await continuation.call(this.#session); + + return response.response; + } + + this.#continuation = Simplify.matching({ + type: Simplify.matching(/^ContinuationItem$/), + }).runOn(this.#page); + + if (this.#continuation) + return this.getContinuationData(); + + return; + } + + async getContinuation() { + return new Feed(this.session, await this.getContinuationData()); + } +} + +module.exports = Feed; diff --git a/lib/core/HomeFeed.js b/lib/core/HomeFeed.js index 45f4d4421..dd66be6b7 100644 --- a/lib/core/HomeFeed.js +++ b/lib/core/HomeFeed.js @@ -1,43 +1,14 @@ -const ResultsParser = require("../parser/contents"); const Simplify = require("../parser/simplify"); const { InnertubeError } = require("../utils/Utils"); -const Utils = require("../utils/Utils"); -const SimpleVideo = require("./SimpleVideo"); +const Feed = require("./Feed"); -class HomeFeed { - #page; - /** - * @type {import('../parser/contents/ContinuationItem')[]} - */ - #continuation; +class HomeFeed extends Feed { /** * @type {import('../parser/contents/ChipCloudChip')[]} */ #chips; - #session; constructor(session, data) { - if (data.on_response_received_actions) - this.#page = data; - else - this.#page = ResultsParser.parseResponse(data); - this.#session = session; - } - - /** - * Get the original page data - */ - get page() { - return this.#page; - } - - /** - * Get all the videos in the home feed - * @returns {Array} - */ - getVideos() { - return Simplify.matching({ - type: Simplify.matching(SimpleVideo.regex), - }).runOn(this.#page); + super(session, data) } /** @@ -49,7 +20,7 @@ class HomeFeed { const chipbars = Simplify.matching({ type: Simplify.matching(/^FeedFilterChipBar$/), - }).runOn(this.#page); + }).runOn(this.page); if (chipbars.length > 1) throw new InnertubeError('There are too many feed filter chipbars, you\'ll need to find the correct one yourself in this.page'); @@ -64,36 +35,8 @@ class HomeFeed { return this.getFeedFilters(); } - /** - * Get all the videos in the home feed - * @returns {SimpleVideo[]} - */ - get videos() { - const simplified = this.getVideos(); - - return simplified.map(video => new SimpleVideo(video)); - } - async getContinuation() { - 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]; - const response = await continuation.call(this.#session); - - return new HomeFeed(this.#session, response.response); - } - - this.#continuation = Simplify.matching({ - type: Simplify.matching(/^ContinuationItem$/), - }).runOn(this.#page); - - if (this.#continuation) - return this.getContinuation(); - - return; + return new HomeFeed(this.session, await this.getContinuationData()); } } From de89d0c78dca37616fbc096c5c6c6217f505c26f Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sun, 1 May 2022 19:13:45 +0200 Subject: [PATCH 21/55] feat: get channel videos --- lib/core/Channel.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/core/Channel.js b/lib/core/Channel.js index f60bb33a8..dd3d484a4 100644 --- a/lib/core/Channel.js +++ b/lib/core/Channel.js @@ -1,5 +1,7 @@ const ResultsParser = require("../parser/contents"); const Simplify = require("../parser/simplify"); +const { InnertubeError } = require("../utils/Utils"); +const Feed = require("./Feed"); const SimpleVideo = require("./SimpleVideo"); class Channel { @@ -10,6 +12,10 @@ class Channel { this.#page = ResultsParser.parseResponse(data); } + get page() { + return this.#page; + } + get title() { return this.metadata.title || ''; } @@ -65,6 +71,18 @@ class Channel { }); } + async getVideos() { + const videos_tab = this.getTab('Videos'); + if (!videos_tab) + throw new InnertubeError('Unable to locate videos tab'); + if (videos_tab.selected) + return new Feed(this.#session, this.page); + const response = await videos_tab.endpoint.call(this.#session); + if (!response.success) + throw new InnertubeError('Failed to get videos', response); + return new Feed(this.#session, response.data); + } + /** * @note home_page only returns videos! * XXX: should some other API be made available to expose the content of the channel @@ -76,7 +94,7 @@ class Channel { return { home_page: this.#getHomePageVideos(), // TODO: - getVideos: () => {}, + getVideos: this.getVideos.bind(this), getPlaylists: () => {}, getCommunity: () => {}, getChannels: () => {}, From 05fb103c3baeb6eac7f096fcd806d400aa359c77 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sun, 1 May 2022 19:56:16 +0200 Subject: [PATCH 22/55] feat: channel playlists --- lib/core/Channel.js | 36 ++++++++++++------- lib/core/Feed.js | 28 +++++++++++++++ lib/core/SimplePlaylist.js | 54 +++++++++++++++++++++++++++++ lib/parser/contents/GridPlaylist.js | 5 ++- 4 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 lib/core/SimplePlaylist.js diff --git a/lib/core/Channel.js b/lib/core/Channel.js index dd3d484a4..eb00fa772 100644 --- a/lib/core/Channel.js +++ b/lib/core/Channel.js @@ -53,6 +53,7 @@ class Channel { }).runOn(this.#page)[0]; } + // TODO: this assumes that the Channel contains a populated Home tab #getHomePageVideos() { const tab = this.getTab('Home'); /** @@ -71,31 +72,40 @@ class Channel { }); } - async getVideos() { - const videos_tab = this.getTab('Videos'); - if (!videos_tab) - throw new InnertubeError('Unable to locate videos tab'); - if (videos_tab.selected) + async getFeedFromTab(tab) { + const target_tab = this.getTab(tab); + if (!target_tab) + throw new InnertubeError('Unable to locate "' + tab + '" tab'); + if (target_tab.selected) return new Feed(this.#session, this.page); - const response = await videos_tab.endpoint.call(this.#session); + const response = await target_tab.endpoint.call(this.#session); if (!response.success) - throw new InnertubeError('Failed to get videos', response); + throw new InnertubeError('Failed to get page', response); return new Feed(this.#session, response.data); } + getVideos() { + return this.getFeedFromTab('Videos'); + } + + getPlaylists() { + return this.getFeedFromTab('Playlists'); + } + + getHome() { + return this.getFeedFromTab('Home'); + } + /** * @note home_page only returns videos! - * XXX: should some other API be made available to expose the content of the channel - * or should I just leave it like this and expect more advanced users - * to use the getTab() method and navigate the tabs themselves using - * the Simplify API to extract what they need? + * @deprecated use getXXX family of functions instead */ get content() { return { home_page: this.#getHomePageVideos(), - // TODO: + getHome: this.getHome.bind(this), getVideos: this.getVideos.bind(this), - getPlaylists: () => {}, + getPlaylists: this.getPlaylists.bind(this), getCommunity: () => {}, getChannels: () => {}, getAbout: () => {} diff --git a/lib/core/Feed.js b/lib/core/Feed.js index 21f74b95e..0417df084 100644 --- a/lib/core/Feed.js +++ b/lib/core/Feed.js @@ -1,5 +1,7 @@ const ResultsParser = require('../parser/contents'); const Simplify = require('../parser/simplify'); +const { InnertubeError } = require('../utils/Utils'); +const SimplePlaylist = require('./SimplePlaylist'); const SimpleVideo = require('./SimpleVideo'); class Feed { @@ -42,6 +44,16 @@ class Feed { }).runOn(this.#page); } + /** + * Get all playlists in the feed + * @returns {Array} + */ + getPlaylists() { + return Simplify.matching({ + type: Simplify.matching(SimplePlaylist.regex), + }).runOn(this.#page); + } + /** * Get unified list of videos * @returns {SimpleVideo[]} @@ -52,6 +64,22 @@ class Feed { return simplified.map(video => new SimpleVideo(video)); } + /** + * Get unified list of playlists + * @returns {SimplePlaylist[]} + */ + get playlists() { + const simplified = this.getPlaylists(); + + return simplified.map(playlist => new SimplePlaylist(playlist)); + } + + get has_continuation() { + return Simplify.matching({ + type: Simplify.matching(/^ContinuationItem$/), + }).runOn(this.#page).length > 0; + } + async getContinuationData() { if (this.#continuation) { if (this.#continuation.length > 1) diff --git a/lib/core/SimplePlaylist.js b/lib/core/SimplePlaylist.js new file mode 100644 index 000000000..bc0f697e2 --- /dev/null +++ b/lib/core/SimplePlaylist.js @@ -0,0 +1,54 @@ +/** + * Wraps around: + * - Playlist + * - GridPlaylist + * + * Provides a unified interface for all of them. + */ +class SimplePlaylist { + /** + * @type {import('../parser/contents/Playlist') | import('../parser/contents/GridPlaylist')} + */ + #wrapper; + /** + * Wrap around a playlist + * @param {import('../parser/contents/Playlist') | import('../parser/contents/GridPlaylist')} playlist + */ + constructor(playlist) { + this.#wrapper = playlist; + } + + get id() { + return this.#wrapper.id; + } + + get title() { + return this.#wrapper.title.toString(); + } + + get thumbnail() { + return this.#wrapper.thumbnails[0]; + } + + get thumbnails() { + return this.#wrapper.thumbnails; + } + + get video_thumbnails() { + // TODO: verify this + return this.#wrapper.video_thumbnails || this.#wrapper.first_videos.map(video => video.thumbnails); + } + + get author() { + return this.#wrapper.author; + } + + // TODO: endpoints? + // view playlist? + + static get regex() { + return /^(Playlist|GridPlaylist)$/; + } +} + +module.exports = SimplePlaylist; diff --git a/lib/parser/contents/GridPlaylist.js b/lib/parser/contents/GridPlaylist.js index a9c9d23e7..d44f62f7e 100644 --- a/lib/parser/contents/GridPlaylist.js +++ b/lib/parser/contents/GridPlaylist.js @@ -7,9 +7,12 @@ class GridPlaylist { type = 'GridPlaylist'; constructor(item) { + // TODO: is an author returned? + // I know there is a ownerBadges, but no name on channel playlist tab + // further investigation needed this.id = item.playlistId; this.videos = new Text(item.videoCountShortText); - this.thumbnauls = Thumbnail.fromResponse(item.thumbnail); + this.thumbnails = Thumbnail.fromResponse(item.thumbnail); this.video_thumbnails = Array.isArray(item.sidebarThumbnails) ? item.sidebarThumbnails.map(thumbs => Thumbnail.fromResponse(thumbs)) : []; this.title = new Text(item.title); this.endpoint = new NavigationEndpoint(item.navigationEndpoint); From b8eb72296fbbf662992de5cd92d47bc8b2425bc6 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sun, 1 May 2022 20:39:25 +0200 Subject: [PATCH 23/55] feat: get channel community posts --- lib/core/Channel.js | 6 +++++- lib/core/Feed.js | 19 ++++++++++++++++++- lib/parser/contents/GridPlaylist.js | 3 ++- lib/parser/contents/Playlist.js | 3 ++- lib/parser/contents/index.js | 5 +++-- 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/lib/core/Channel.js b/lib/core/Channel.js index eb00fa772..40410dc5c 100644 --- a/lib/core/Channel.js +++ b/lib/core/Channel.js @@ -96,6 +96,10 @@ class Channel { return this.getFeedFromTab('Home'); } + getCommunity() { + return this.getFeedFromTab('Community'); + } + /** * @note home_page only returns videos! * @deprecated use getXXX family of functions instead @@ -106,7 +110,7 @@ class Channel { getHome: this.getHome.bind(this), getVideos: this.getVideos.bind(this), getPlaylists: this.getPlaylists.bind(this), - getCommunity: () => {}, + getCommunity: this.getCommunity.bind(this), getChannels: () => {}, getAbout: () => {} } diff --git a/lib/core/Feed.js b/lib/core/Feed.js index 0417df084..753a890bc 100644 --- a/lib/core/Feed.js +++ b/lib/core/Feed.js @@ -15,7 +15,7 @@ class Feed { */ #session; constructor(session, data) { - if (data.on_response_received_actions) + if (data.on_response_received_actions || data.on_response_received_endpoints) this.#page = data; else this.#page = ResultsParser.parseResponse(data); @@ -54,6 +54,16 @@ class Feed { }).runOn(this.#page); } + /** + * Get all the community posts in the feed + * @returns {import('../parser/contents/BackstagePost')[]} + */ + getBackstagePosts() { + return Simplify.matching({ + type: Simplify.matching(/^BackstagePost$/), + }).runOn(this.#page); + } + /** * Get unified list of videos * @returns {SimpleVideo[]} @@ -74,6 +84,13 @@ class Feed { return simplified.map(playlist => new SimplePlaylist(playlist)); } + /** + * Get a list of community posts + */ + get backstage_posts() { + return this.getBackstagePosts(); + } + get has_continuation() { return Simplify.matching({ type: Simplify.matching(/^ContinuationItem$/), diff --git a/lib/parser/contents/GridPlaylist.js b/lib/parser/contents/GridPlaylist.js index d44f62f7e..6f4afebbc 100644 --- a/lib/parser/contents/GridPlaylist.js +++ b/lib/parser/contents/GridPlaylist.js @@ -1,4 +1,5 @@ const ResultsParser = require('.'); +const NavigatableText = require('./NavigatableText'); const NavigationEndpoint = require('./NavigationEndpoint'); const Text = require('./Text'); const Thumbnail = require('./Thumbnail'); @@ -16,7 +17,7 @@ class GridPlaylist { this.video_thumbnails = Array.isArray(item.sidebarThumbnails) ? item.sidebarThumbnails.map(thumbs => Thumbnail.fromResponse(thumbs)) : []; this.title = new Text(item.title); this.endpoint = new NavigationEndpoint(item.navigationEndpoint); - this.view_playlist = new Text(item.viewPlaylistText); + this.view_playlist = new NavigatableText(item.viewPlaylistText); } } diff --git a/lib/parser/contents/Playlist.js b/lib/parser/contents/Playlist.js index 0d32073e2..5a78413f0 100644 --- a/lib/parser/contents/Playlist.js +++ b/lib/parser/contents/Playlist.js @@ -1,5 +1,6 @@ const ResultsParser = require('.'); const Author = require('./Author'); +const NavigatableText = require('./NavigatableText'); const NavigationEndpoint = require('./NavigationEndpoint'); const Text = require('./Text'); const Thumbnail = require('./Thumbnail'); @@ -16,7 +17,7 @@ class Playlist { this.videos = parseInt(item.videoCount); this.first_videos = ResultsParser.parse(item.videos); this.endpoint = new NavigationEndpoint(item.navigationEndpoint); - this.view_playlist = new Text(item.viewPlaylistText); + this.view_playlist = new NavigatableText(item.viewPlaylistText); } } diff --git a/lib/parser/contents/index.js b/lib/parser/contents/index.js index 7fb42eca9..8548123af 100644 --- a/lib/parser/contents/index.js +++ b/lib/parser/contents/index.js @@ -10,7 +10,8 @@ class ResultsParser { static parseResponse(data) { return { contents: data.contents && ResultsParser.parseItem(data.contents), - on_response_received_actions: data.onResponseReceivedActions && ResultsParser.parseRRA(data.onResponseReceivedActions) || undefined, + on_response_received_actions: data.onResponseReceivedActions && ResultsParser.parseRR(data.onResponseReceivedActions) || undefined, + on_response_received_endpoints: data.onResponseReceivedEndpoints && ResultsParser.parseRR(data.onResponseReceivedEndpoints) || undefined, metadata: data.metadata && ResultsParser.parseItem(data.metadata), header: data.header && ResultsParser.parseItem(data.header), // XXX: microformat contains meta tags for SEO? @@ -18,7 +19,7 @@ class ResultsParser { } } - static parseRRA(actions) { + static parseRR(actions) { return actions.map((action) => { if (Object.keys(action).includes('appendContinuationItemsAction')) return new AppendContinuationItemsAction(action.appendContinuationItemsAction); From 8486fe67b8a7d1fd34d0a601b713bfba3312aaea Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sun, 1 May 2022 21:02:42 +0200 Subject: [PATCH 24/55] feat: get channels from channel --- lib/core/Channel.js | 6 +++++- lib/core/Feed.js | 17 +++++++++++++++++ lib/parser/contents/Channel.js | 2 +- lib/parser/contents/GridChannel.js | 10 ++++++---- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/lib/core/Channel.js b/lib/core/Channel.js index 40410dc5c..ff1899c2a 100644 --- a/lib/core/Channel.js +++ b/lib/core/Channel.js @@ -100,6 +100,10 @@ class Channel { return this.getFeedFromTab('Community'); } + getChannels() { + return this.getFeedFromTab('Channels'); + } + /** * @note home_page only returns videos! * @deprecated use getXXX family of functions instead @@ -111,7 +115,7 @@ class Channel { getVideos: this.getVideos.bind(this), getPlaylists: this.getPlaylists.bind(this), getCommunity: this.getCommunity.bind(this), - getChannels: () => {}, + getChannels: this.getChannels.bind(this), getAbout: () => {} } } diff --git a/lib/core/Feed.js b/lib/core/Feed.js index 753a890bc..2890d0b14 100644 --- a/lib/core/Feed.js +++ b/lib/core/Feed.js @@ -4,6 +4,8 @@ const { InnertubeError } = require('../utils/Utils'); const SimplePlaylist = require('./SimplePlaylist'); const SimpleVideo = require('./SimpleVideo'); +// TODO: add a way subdivide into sections and return subfeeds? + class Feed { #page; /** @@ -64,6 +66,16 @@ class Feed { }).runOn(this.#page); } + /** + * Get all the channels in the feed + * @returns {Array} + */ + getChannels() { + return Simplify.matching({ + type: Simplify.matching(/^(Channel|GridChannel)$/), + }).runOn(this.#page); + } + /** * Get unified list of videos * @returns {SimpleVideo[]} @@ -84,6 +96,11 @@ class Feed { return simplified.map(playlist => new SimplePlaylist(playlist)); } + get channels() { + // XXX: Channel and GridChannel is so similar we don't need to do anything special + return this.getChannels(); + } + /** * Get a list of community posts */ diff --git a/lib/parser/contents/Channel.js b/lib/parser/contents/Channel.js index f15dba951..e5f8f78f0 100644 --- a/lib/parser/contents/Channel.js +++ b/lib/parser/contents/Channel.js @@ -13,9 +13,9 @@ class Channel { navigationEndpoint: item.navigationEndpoint }, item.ownerBadges, item.thumbnail); this.subscribers = new Text(item.subscriberCountText); - this.description_snippet = new Text(item.descriptionSnippet); this.videos = new Text(item.videoCountText); this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + this.description_snippet = new Text(item.descriptionSnippet); } } diff --git a/lib/parser/contents/GridChannel.js b/lib/parser/contents/GridChannel.js index c80638a74..b0e41c4c1 100644 --- a/lib/parser/contents/GridChannel.js +++ b/lib/parser/contents/GridChannel.js @@ -1,17 +1,19 @@ const ResultsParser = require('.'); +const Author = require('./Author'); const NavigationEndpoint = require('./NavigationEndpoint'); const Text = require('./Text'); -const Thumbnail = require('./Thumbnail'); class GridChannel { type = 'GridChannel'; constructor(item) { this.id = item.channelId; - this.thumbnails = Thumbnail.fromResponse(item.thumbnail); - this.videos = new Text(item.videoCountText); + this.author = new Author({ + ...item.title, + navigationEndpoint: item.navigationEndpoint + }, item.ownerBadges, item.thumbnail); this.subscribers = new Text(item.subscriberCountText); - this.name = new Text(item.title); + this.videos = new Text(item.videoCountText); this.endpoint = new NavigationEndpoint(item.navigationEndpoint); } } From cc957b70cf53b84b38099957918a8fef6346874c Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sun, 1 May 2022 21:11:47 +0200 Subject: [PATCH 25/55] feat: get channel about page data --- lib/core/Channel.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/core/Channel.js b/lib/core/Channel.js index ff1899c2a..37981fca9 100644 --- a/lib/core/Channel.js +++ b/lib/core/Channel.js @@ -104,6 +104,25 @@ class Channel { return this.getFeedFromTab('Channels'); } + /** + * Get the channel about page + * @returns {import('../parser/contents/ChannelAboutFullMetadata')} + */ + async getAbout() { + const target_tab = this.getTab('About'); + if (!target_tab) + throw new InnertubeError('Unable to locate "About" tab'); + if (target_tab.selected) + return new Feed(this.#session, this.page); + const response = await target_tab.endpoint.call(this.#session); + if (!response.success) + throw new InnertubeError('Failed to get page', response); + const parsed = ResultsParser.parseResponse(response.data); + return Simplify.matching({ + type: Simplify.matching(/^ChannelAboutFullMetadata$/), + }).runOn(parsed)[0]; + } + /** * @note home_page only returns videos! * @deprecated use getXXX family of functions instead @@ -116,7 +135,7 @@ class Channel { getPlaylists: this.getPlaylists.bind(this), getCommunity: this.getCommunity.bind(this), getChannels: this.getChannels.bind(this), - getAbout: () => {} + getAbout: this.getAbout.bind(this), } } } From 33c134c51c6bc911795f7c3c4f7b3ef3cfc6d770 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sun, 1 May 2022 22:42:10 +0200 Subject: [PATCH 26/55] feat: add missing channel parsers this commit also adds regenerated types I've neglected to push --- examples/channel.js | 40 +++++++++++++ lib/core/Channel.js | 4 +- lib/core/Feed.js | 4 +- lib/parser/contents/Button.js | 3 + lib/parser/contents/C4TabbedHeader.js | 24 ++++++++ lib/parser/contents/ChannelHeaderLinks.js | 23 +++++++ lib/parser/contents/MicroformatData.js | 35 +++++++++++ lib/parser/contents/NavigatableText.js | 4 -- lib/parser/contents/Shelf.js | 6 +- lib/parser/contents/index.js | 7 ++- typings/lib/Innertube.d.ts | 34 ++--------- typings/lib/core/Channel.d.ts | 58 ++++++++++++++++++ typings/lib/core/Feed.d.ts | 50 ++++++++++++++++ typings/lib/core/HomeFeed.d.ts | 50 +--------------- typings/lib/core/Livechat.d.ts | 5 +- typings/lib/core/SimplePlaylist.d.ts | 23 +++++++ typings/lib/core/SimpleVideo.d.ts | 41 +++++++++++++ typings/lib/core/Trending.d.ts | 60 +++++++++++++++++++ typings/lib/deciphers/Signature.d.ts | 2 +- typings/lib/parser/contents/Author.d.ts | 21 +++---- typings/lib/parser/contents/Button.d.ts | 3 + .../lib/parser/contents/C4TabbedHeader.d.ts | 16 +++++ typings/lib/parser/contents/Channel.d.ts | 2 +- .../parser/contents/ChannelHeaderLinks.d.ts | 7 +++ .../lib/parser/contents/ChannelMetadata.d.ts | 24 ++++---- typings/lib/parser/contents/CompactVideo.d.ts | 1 + .../lib/parser/contents/ContinuationItem.d.ts | 4 ++ typings/lib/parser/contents/GridChannel.d.ts | 7 +-- typings/lib/parser/contents/GridPlaylist.d.ts | 5 +- typings/lib/parser/contents/GridVideo.d.ts | 4 ++ .../lib/parser/contents/MetadataBadge.d.ts | 5 ++ .../lib/parser/contents/MicroformatData.d.ts | 29 +++++++++ .../lib/parser/contents/NavigatableText.d.ts | 1 - .../parser/contents/NavigationEndpoint.d.ts | 14 ++++- typings/lib/parser/contents/Playlist.d.ts | 3 +- .../parser/contents/PlaylistPanelVideo.d.ts | 3 +- .../lib/parser/contents/PlaylistVideo.d.ts | 7 ++- typings/lib/parser/contents/Shelf.d.ts | 6 +- typings/lib/parser/contents/Tab.d.ts | 5 +- typings/lib/parser/contents/Text.d.ts | 14 ++++- typings/lib/parser/contents/Thumbnail.d.ts | 20 +++---- typings/lib/parser/contents/index.d.ts | 6 +- typings/lib/proto/index.d.ts | 2 +- typings/lib/utils/Request.d.ts | 2 +- 44 files changed, 530 insertions(+), 154 deletions(-) create mode 100644 examples/channel.js create mode 100644 lib/parser/contents/C4TabbedHeader.js create mode 100644 lib/parser/contents/ChannelHeaderLinks.js create mode 100644 lib/parser/contents/MicroformatData.js create mode 100644 typings/lib/core/Channel.d.ts create mode 100644 typings/lib/core/Feed.d.ts create mode 100644 typings/lib/core/SimplePlaylist.d.ts create mode 100644 typings/lib/core/SimpleVideo.d.ts create mode 100644 typings/lib/core/Trending.d.ts create mode 100644 typings/lib/parser/contents/C4TabbedHeader.d.ts create mode 100644 typings/lib/parser/contents/ChannelHeaderLinks.d.ts create mode 100644 typings/lib/parser/contents/MicroformatData.d.ts diff --git a/examples/channel.js b/examples/channel.js new file mode 100644 index 000000000..f33f77da4 --- /dev/null +++ b/examples/channel.js @@ -0,0 +1,40 @@ +const Innertube = require('..'); + +(async () => { + +const session = await new Innertube(); + +const channel = await session.getChannel('UCX6OQ3DkcsbYNE6H8uQQuVA'); +const home = await channel.getHome(); + +console.log('Viewing channel:', channel.title); +console.log('Family Safe:', channel.metadata.is_family_safe); +const about = await channel.getAbout(); +console.log('Country:', about.country.toString()); + + +console.log('\nLists the following videos:'); +const videos = await channel.getVideos(); +for (const video of videos.videos) { + console.log('Video:', video.title); +} + +console.log('\nLists the following playlists:'); +const playlists = await channel.getPlaylists(); +for (const playlist of playlists.playlists) { + console.log('Playlist:', playlist.title); +} + +console.log('\nLists the following channels:'); +const channels = await channel.getChannels(); +for (const channel of channels.channels) { + console.log('Channel:', channel.author.name); +} + +console.log('\nLists the following community posts:'); +const posts = await channel.getCommunity(); +for (const post of posts.backstage_posts) { + console.log('Backstage post:', post.content.toString().substring(0, 20) + '...'); +} + +})(); \ No newline at end of file diff --git a/lib/core/Channel.js b/lib/core/Channel.js index 37981fca9..6f433fad4 100644 --- a/lib/core/Channel.js +++ b/lib/core/Channel.js @@ -77,7 +77,7 @@ class Channel { if (!target_tab) throw new InnertubeError('Unable to locate "' + tab + '" tab'); if (target_tab.selected) - return new Feed(this.#session, this.page); + return new Feed(this.#session, this.page, true); const response = await target_tab.endpoint.call(this.#session); if (!response.success) throw new InnertubeError('Failed to get page', response); @@ -106,7 +106,7 @@ class Channel { /** * Get the channel about page - * @returns {import('../parser/contents/ChannelAboutFullMetadata')} + * @returns {Promise} */ async getAbout() { const target_tab = this.getTab('About'); diff --git a/lib/core/Feed.js b/lib/core/Feed.js index 2890d0b14..c8e1a2141 100644 --- a/lib/core/Feed.js +++ b/lib/core/Feed.js @@ -16,8 +16,8 @@ class Feed { * @type {import('../Innertube')} */ #session; - constructor(session, data) { - if (data.on_response_received_actions || data.on_response_received_endpoints) + constructor(session, 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); diff --git a/lib/parser/contents/Button.js b/lib/parser/contents/Button.js index f73538b4b..99c139990 100644 --- a/lib/parser/contents/Button.js +++ b/lib/parser/contents/Button.js @@ -10,6 +10,9 @@ class Button { this.service_endpoint = item.serviceEndpoint && new NavigationEndpoint(item.serviceEndpoint); this.text = new Text(item.text); this.tooltip = item.tooltip; + this.icon = item.icon ? { + icon_type: item.icon.iconType, + } : null; } get endpoint() { diff --git a/lib/parser/contents/C4TabbedHeader.js b/lib/parser/contents/C4TabbedHeader.js new file mode 100644 index 000000000..e01645cd4 --- /dev/null +++ b/lib/parser/contents/C4TabbedHeader.js @@ -0,0 +1,24 @@ +const ResultsParser = require('.'); +const Author = require('./Author'); +const Thumbnail = require('./Thumbnail'); +const Text = require('./Text'); + +class C4TabbedHeader { + type = 'C4TabbedHeader'; + + constructor(item) { + this.author = new Author({ + simpleText: item.title, + navigationEndpoint: item.navigationEndpoint + }, item.badges, item.avatar); + this.banner = item.banner && Thumbnail.fromResponse(item.banner) || []; + this.tv_banner = item.tvBanner && Thumbnail.fromResponse(item.tvBanner) || []; + this.mobile_banner = item.mobileBanner && Thumbnail.fromResponse(item.mobileBanner) || []; + this.subscribers = new Text(item.subscriberCountText); + this.sponsor_button = item.sponsorButton && ResultsParser.parseItem(item.sponsorButton); + this.subscribe_button = item.subscribeButton && ResultsParser.parseItem(item.subscribeButton); + this.header_links = item.headerLinks && ResultsParser.parseItem(item.headerLinks); + } +} + +module.exports = C4TabbedHeader; diff --git a/lib/parser/contents/ChannelHeaderLinks.js b/lib/parser/contents/ChannelHeaderLinks.js new file mode 100644 index 000000000..67694262b --- /dev/null +++ b/lib/parser/contents/ChannelHeaderLinks.js @@ -0,0 +1,23 @@ +const ResultsParser = require('.'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); +const Thumbnail = require('./Thumbnail'); + +class HeaderLink { + constructor(item) { + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + this.icon = Thumbnail.fromResponse(item.icon); + this.title = new Text(item.title); + } +} + +class ChannelHeaderLinks { + type = 'ChannelHeaderLinks'; + + constructor(item) { + this.primary = item.primaryLinks.map(link => new HeaderLink(link)); + this.secondary = item.secondaryLinks.map(link => new HeaderLink(link)); + } +} + +module.exports = ChannelHeaderLinks; \ No newline at end of file diff --git a/lib/parser/contents/MicroformatData.js b/lib/parser/contents/MicroformatData.js new file mode 100644 index 000000000..89c73d1ea --- /dev/null +++ b/lib/parser/contents/MicroformatData.js @@ -0,0 +1,35 @@ +const ResultsParser = require('.'); +const Thumbnail = require('./Thumbnail'); + +class MicroformatData { + type = 'MicroformatData'; + + constructor(item) { + this.url_canonical = item.urlCanonical; + this.title = item.title; + this.description = item.description; + this.thumbnail = Thumbnail.fromResponse(item.thumbnail); + this.site_name = item.siteName; + this.app_name = item.appName; + this.android_package = item.androidPackage; + this.ios_app_store_id = item.iosAppStoreId; + this.ios_app_arguments = item.iosAppArguments; + this.og_type = item.ogType; + this.url_applinks_web = item.urlApplinksWeb; + this.url_applinks_ios = item.urlApplinksIos; + this.url_applinks_android = item.urlApplinksAndroid; + this.url_twitter_ios = item.urlTwitterIos; + this.url_twitter_android = item.urlTwitterAndroid; + this.twitter_card_type = item.twitterCardType; + this.twitter_site_handle = item.twitterSiteHandle; + this.schema_dot_org_type = item.schemaDotOrgType; + this.noindex = item.noindex; + this.is_unlisted = item.unlisted; + this.is_family_safe = item.familySafe; + this.tags = item.tags; + this.available_countries = item.availableCountries; + // XXX: linkAlternatives? + } +} + +module.exports = MicroformatData; \ No newline at end of file diff --git a/lib/parser/contents/NavigatableText.js b/lib/parser/contents/NavigatableText.js index c6f9f7e53..c3ec6e687 100644 --- a/lib/parser/contents/NavigatableText.js +++ b/lib/parser/contents/NavigatableText.js @@ -16,10 +16,6 @@ class NavigatableText extends Text { new NavigationEndpoint(node.titleNavigationEndpoint) : null; } - toString() { - return `[${this.text}](${this.url?.toString()})`; - } - toJSON() { return this; } diff --git a/lib/parser/contents/Shelf.js b/lib/parser/contents/Shelf.js index bbd725eaf..c10f9faee 100644 --- a/lib/parser/contents/Shelf.js +++ b/lib/parser/contents/Shelf.js @@ -11,11 +11,7 @@ class Shelf { this.endpoint = item.endpoint ? new NavigationEndpoint(item.endpoint) : null; // XXX: maybe add this as buttonRenderer? // this is the playAllButton in the original response - this.button = item.playAllButton?.buttonRenderer ? { - text: new Text(item.playAllButton.buttonRenderer.text, ''), - endpoint: item.playAllButton.buttonRenderer.navigationEndpoint ? new NavigationEndpoint(item.playAllButton.buttonRenderer.navigationEndpoint) : null, - icon: item.playAllButton.buttonRenderer.icon?.iconType || 'UNKNOWN' - } : null; + this.button = item.playAllButton && ResultsParser.parseItem(item.playAllButton); } } diff --git a/lib/parser/contents/index.js b/lib/parser/contents/index.js index 8548123af..a969f7a50 100644 --- a/lib/parser/contents/index.js +++ b/lib/parser/contents/index.js @@ -95,13 +95,18 @@ class ResultsParser { movingThumbnailRenderer: require('./MovingThumbnail'), expandedShelfContentsRenderer: require('./GenericList')('ExpandedShelfContents', 'items'), channelMetadataRenderer: require('./ChannelMetadata'), + c4TabbedHeaderRenderer: require('./C4TabbedHeader'), + microformatDataRenderer: require('./MicroformatData'), + channelHeaderLinksRenderer: require('./ChannelHeaderLinks'), } const keys = Reflect.ownKeys(item); if (keys.length !== 1) return null; if (!renderers.hasOwnProperty(keys[0])) { - console.warn('No renderer found for type: ', keys[0]); + console.warn( + 'No renderer found for type: ' + keys[0] + + '\nThis is a bug, please report it at ' + require('../../../package.json').bugs.url); return null; } diff --git a/typings/lib/Innertube.d.ts b/typings/lib/Innertube.d.ts index 968358cbf..ef51fec97 100644 --- a/typings/lib/Innertube.d.ts +++ b/typings/lib/Innertube.d.ts @@ -364,14 +364,9 @@ declare class Innertube { * Retrieves contents for a given channel. (WIP) * * @param {string} id - channel id - * @return {Promise.<{ title: string; description: string; metadata: object; content: object }>} + * @return {Promise} */ - getChannel(id: string): Promise<{ - title: string; - description: string; - metadata: object; - content: object; - }>; + getChannel(id: string): Promise; /** * Retrieves watch history. * @returns {Promise.<{ items: [{ date: string; videos: [] }] }>} @@ -390,27 +385,9 @@ declare class Innertube { /** * Retrieves trending content. * - * @returns {Promise.<{ now: { content: [{ title: string; videos: []; }] }; - * music: { getVideos: Promise.; }; gaming: { getVideos: Promise.; }; - * gaming: { getVideos: Promise.; }; }>} + * @returns {Promise} */ - getTrending(): Promise<{ - now: { - content: [{ - title: string; - videos: []; - }]; - }; - music: { - getVideos: Promise; - }; - gaming: { - getVideos: Promise; - }; - gaming: { - getVideos: Promise; - }; - }>; + getTrending(): Promise; getLibrary(): Promise; /** * Retrieves subscriptions feed. @@ -515,7 +492,8 @@ declare class Innertube { }): ReadableStream; #private; } -import EventEmitter = require("events"); import Request = require("./utils/Request"); import Actions = require("./core/Actions"); +import Channel = require("./core/Channel"); import HomeFeed = require("./core/HomeFeed"); +import { Trending } from "./core/Trending"; diff --git a/typings/lib/core/Channel.d.ts b/typings/lib/core/Channel.d.ts new file mode 100644 index 000000000..7a93952da --- /dev/null +++ b/typings/lib/core/Channel.d.ts @@ -0,0 +1,58 @@ +export = Channel; +declare class Channel { + constructor(session: any, data: any); + get page(): { + contents: any; + on_response_received_actions: any; + on_response_received_endpoints: any; + metadata: any; + header: any; + microformat: any; + }; + get title(): any; + get description(): any; + /** + * @type {import('../parser/contents/ChannelMetadata')} + */ + get metadata(): import("../parser/contents/ChannelMetadata"); + /** + * + * @returns {import('../parser/contents/Tab')[]} + */ + getTabs(): import('../parser/contents/Tab')[]; + /** + * + * @param {string} name + * @returns {import('../parser/contents/Tab')} + */ + getTab(name: string): import('../parser/contents/Tab'); + getFeedFromTab(tab: any): Promise; + getVideos(): Promise; + getPlaylists(): Promise; + getHome(): Promise; + getCommunity(): Promise; + getChannels(): Promise; + /** + * Get the channel about page + * @returns {Promise} + */ + getAbout(): Promise; + /** + * @note home_page only returns videos! + * @deprecated use getXXX family of functions instead + */ + get content(): { + home_page: { + title: string; + content: any; + }[]; + getHome: any; + getVideos: any; + getPlaylists: any; + getCommunity: any; + getChannels: any; + getAbout: any; + }; + #private; +} +import Feed = require("./Feed"); diff --git a/typings/lib/core/Feed.d.ts b/typings/lib/core/Feed.d.ts new file mode 100644 index 000000000..4582f9383 --- /dev/null +++ b/typings/lib/core/Feed.d.ts @@ -0,0 +1,50 @@ +export = Feed; +declare class Feed { + constructor(session: any, data: any, already_parsed?: boolean); + /** + * Get the original page data + */ + get page(): any; + get session(): import("../Innertube"); + /** + * Get all the videos in the feed + * @returns {Array} + */ + getVideos(): Array; + /** + * Get all playlists in the feed + * @returns {Array} + */ + getPlaylists(): Array; + /** + * Get all the community posts in the feed + * @returns {import('../parser/contents/BackstagePost')[]} + */ + getBackstagePosts(): import('../parser/contents/BackstagePost')[]; + /** + * Get all the channels in the feed + * @returns {Array} + */ + getChannels(): Array; + /** + * Get unified list of videos + * @returns {SimpleVideo[]} + */ + get videos(): SimpleVideo[]; + /** + * Get unified list of playlists + * @returns {SimplePlaylist[]} + */ + get playlists(): SimplePlaylist[]; + get channels(): (import("../parser/contents/Channel") | import("../parser/contents/GridChannel"))[]; + /** + * Get a list of community posts + */ + get backstage_posts(): import("../parser/contents/BackstagePost")[]; + get has_continuation(): boolean; + getContinuationData(): any; + getContinuation(): Promise; + #private; +} +import SimpleVideo = require("./SimpleVideo"); +import SimplePlaylist = require("./SimplePlaylist"); diff --git a/typings/lib/core/HomeFeed.d.ts b/typings/lib/core/HomeFeed.d.ts index 99d5375b4..0575be434 100644 --- a/typings/lib/core/HomeFeed.d.ts +++ b/typings/lib/core/HomeFeed.d.ts @@ -1,56 +1,12 @@ export = HomeFeed; -declare class HomeFeed { +declare class HomeFeed extends Feed { constructor(session: any, data: any); - /** - * Get the original page data - */ - get page(): any; - /** - * Get all the videos in the home feed - * @returns {Array} - */ - getVideos(): Array; /** * Get filters for the home feed * @returns {import('../parser/contents/ChipCloudChip')[]} */ getFeedFilters(): import('../parser/contents/ChipCloudChip')[]; - /** - * Get all the videos in the home feed - * @deprecated Use getVideos instead - */ - get videos(): { - id: string; - title: any; - description: string; - channel: { - id: any; - name: string; - url: any; - thumbnail: import("../parser/contents/Thumbnail"); - is_verified: boolean; - is_verified_artist: boolean; - }; - metadata: { - view_count: any; - short_view_count_text: { - simple_text: any; - accessibility_label: string; - }; - thumbnail: import("../parser/contents/Thumbnail"); - thumbnails: import("../parser/contents/Thumbnail")[]; - moving_thumbnail: {}; - moving_thumbnails: import("../parser/contents/Thumbnail")[]; - published: any; - duration: { - seconds: number; - simple_text: string; - accessibility_label: string; - }; - badges: import("../parser/contents/MetadataBadge")[]; - owner_badges: import("../parser/contents/MetadataBadge")[]; - }; - }[]; - getContinuation(): any; + getContinuation(): Promise; #private; } +import Feed = require("./Feed"); diff --git a/typings/lib/core/Livechat.d.ts b/typings/lib/core/Livechat.d.ts index f709737f9..a054b698b 100644 --- a/typings/lib/core/Livechat.d.ts +++ b/typings/lib/core/Livechat.d.ts @@ -1,5 +1,5 @@ export = Livechat; -declare class Livechat extends EventEmitter { +declare class Livechat { constructor(session: any, token: any, channel_id: any, video_id: any); ctoken: any; session: any; @@ -10,7 +10,7 @@ declare class Livechat extends EventEmitter { poll_intervals_ms: number; running: boolean; metadata_ctoken: any; - livechat_poller: NodeJS.Timeout; + livechat_poller: any; sendMessage(text: any): Promise; /** * Blocks a user. @@ -21,4 +21,3 @@ declare class Livechat extends EventEmitter { stop(): void; #private; } -import EventEmitter = require("events"); diff --git a/typings/lib/core/SimplePlaylist.d.ts b/typings/lib/core/SimplePlaylist.d.ts new file mode 100644 index 000000000..06935e01a --- /dev/null +++ b/typings/lib/core/SimplePlaylist.d.ts @@ -0,0 +1,23 @@ +export = SimplePlaylist; +/** + * Wraps around: + * - Playlist + * - GridPlaylist + * + * Provides a unified interface for all of them. + */ +declare class SimplePlaylist { + static get regex(): RegExp; + /** + * Wrap around a playlist + * @param {import('../parser/contents/Playlist') | import('../parser/contents/GridPlaylist')} playlist + */ + constructor(playlist: import('../parser/contents/Playlist') | import('../parser/contents/GridPlaylist')); + get id(): any; + get title(): string; + get thumbnail(): any; + get thumbnails(): any; + get video_thumbnails(): any; + get author(): any; + #private; +} diff --git a/typings/lib/core/SimpleVideo.d.ts b/typings/lib/core/SimpleVideo.d.ts new file mode 100644 index 000000000..fbe5b9331 --- /dev/null +++ b/typings/lib/core/SimpleVideo.d.ts @@ -0,0 +1,41 @@ +export = SimpleVideo; +/** + * Wraps around: + * - Video + * - GridVideo + * - CompactVideo + * - PlaylistVideo + * - PlaylistPanelVideo + * - TODO: WatchCardCompactVideo + * + * Provides a unified interface for all of them. + */ +declare class SimpleVideo { + static get regex(): RegExp; + constructor(video: any); + getUnderlyingRenderer(): import("../parser/contents/Video") | import("../parser/contents/GridVideo") | import("../parser/contents/PlaylistVideo") | import("../parser/contents/CompactVideo") | import("../parser/contents/PlaylistPanelVideo"); + get id(): any; + get title(): string; + get description(): any; + get channel(): import("../parser/contents/Author"); + get metadata(): { + view_count: any; + short_view_count_text: { + simple_text: any; + accessibility_label: string; + }; + thumbnail: import("../parser/contents/Thumbnail"); + thumbnails: import("../parser/contents/Thumbnail")[]; + moving_thumbnail: any; + moving_thumbnails: any; + published: any; + duration: { + seconds: number; + simple_text: string; + accessibility_label: string; + }; + badges: any; + owner_badges: string[]; + }; + #private; +} diff --git a/typings/lib/core/Trending.d.ts b/typings/lib/core/Trending.d.ts new file mode 100644 index 000000000..192d028c7 --- /dev/null +++ b/typings/lib/core/Trending.d.ts @@ -0,0 +1,60 @@ +export class Trending { + constructor(session: any, data: any); + /** + * + * @param {string} title title of the tab to get + * @returns + */ + getTab(title: string): TrendingTab; + /** + * @alias getTab('now') + */ + get now(): TrendingTab; + /** + * @alias getTab('music') + */ + get music(): TrendingTab; + get page(): { + contents: any; + on_response_received_actions: any; + on_response_received_endpoints: any; + metadata: any; + header: any; + microformat: any; + }; + #private; +} +export class TrendingTab { + /** + * @param {import('../parser/contents/Tab')} tab + */ + constructor(session: any, tab: import('../parser/contents/Tab')); + get title(): string; + /** + * @returns {import('../parser/contents/Shelf')[]} + */ + getShelfs(): import('../parser/contents/Shelf')[]; + getShelf(title: any): any; + /** + * @type {{ + * title: string, + * videos: SimpleVideo[] + * }[] | null} + */ + get content(): { + title: string; + videos: SimpleVideo[]; + }[]; + /** + * Selects this tab and returns a new TrendingTab with this tab selected + */ + getSelected(): Promise; + /** + * @note getVideos returns only the vidoes of the first shelf + * @returns {SimpleVideo[]} + */ + getVideos(): SimpleVideo[]; + get raw(): import("../parser/contents/Tab"); + #private; +} +import SimpleVideo = require("./SimpleVideo"); diff --git a/typings/lib/deciphers/Signature.d.ts b/typings/lib/deciphers/Signature.d.ts index 65fa00599..cb737e13e 100644 --- a/typings/lib/deciphers/Signature.d.ts +++ b/typings/lib/deciphers/Signature.d.ts @@ -8,6 +8,6 @@ declare class Signature { /** * Deciphers signature. */ - decipher(): string; + decipher(): any; #private; } diff --git a/typings/lib/parser/contents/Author.d.ts b/typings/lib/parser/contents/Author.d.ts index e0ebd52bd..7b245d82e 100644 --- a/typings/lib/parser/contents/Author.d.ts +++ b/typings/lib/parser/contents/Author.d.ts @@ -2,28 +2,29 @@ export = Author; declare class Author { constructor(item: any, badges: any, thumbs: any); /** - * @type {import('./MetadataBadge')[]} - */ + * @type {import('./MetadataBadge')[]} + */ badges: import('./MetadataBadge')[]; /** - * @type {Thumbnail[]} - */ + * @type {Thumbnail[]} + */ thumbnails: Thumbnail[]; + get url(): any; set name(arg: string); get name(): string; get endpoint(): import("./NavigationEndpoint"); get id(): any; /** - * @type {boolean} - */ + * @type {boolean} + */ get is_verified(): boolean; /** - * @type {boolean} - */ + * @type {boolean} + */ get is_verified_artist(): boolean; /** - * @type {Thumbnail | undefined} - */ + * @type {Thumbnail | undefined} + */ get best_thumbnail(): Thumbnail; #private; } diff --git a/typings/lib/parser/contents/Button.d.ts b/typings/lib/parser/contents/Button.d.ts index a84d6f530..dca64690c 100644 --- a/typings/lib/parser/contents/Button.d.ts +++ b/typings/lib/parser/contents/Button.d.ts @@ -6,6 +6,9 @@ declare class Button { service_endpoint: NavigationEndpoint; text: Text; tooltip: any; + icon: { + icon_type: any; + }; get endpoint(): NavigationEndpoint; } import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/C4TabbedHeader.d.ts b/typings/lib/parser/contents/C4TabbedHeader.d.ts new file mode 100644 index 000000000..515a8027f --- /dev/null +++ b/typings/lib/parser/contents/C4TabbedHeader.d.ts @@ -0,0 +1,16 @@ +export = C4TabbedHeader; +declare class C4TabbedHeader { + constructor(item: any); + type: string; + author: Author; + banner: Thumbnail[]; + tv_banner: Thumbnail[]; + mobile_banner: Thumbnail[]; + subscribers: Text; + sponsor_button: any; + subscribe_button: any; + header_links: any; +} +import Author = require("./Author"); +import Thumbnail = require("./Thumbnail"); +import Text = require("./Text"); diff --git a/typings/lib/parser/contents/Channel.d.ts b/typings/lib/parser/contents/Channel.d.ts index f5f3d5f08..9f6480e3b 100644 --- a/typings/lib/parser/contents/Channel.d.ts +++ b/typings/lib/parser/contents/Channel.d.ts @@ -5,9 +5,9 @@ declare class Channel { id: any; author: Author; subscribers: Text; - description_snippet: Text; videos: Text; endpoint: NavigationEndpoint; + description_snippet: Text; } import Author = require("./Author"); import Text = require("./Text"); diff --git a/typings/lib/parser/contents/ChannelHeaderLinks.d.ts b/typings/lib/parser/contents/ChannelHeaderLinks.d.ts new file mode 100644 index 000000000..3203982eb --- /dev/null +++ b/typings/lib/parser/contents/ChannelHeaderLinks.d.ts @@ -0,0 +1,7 @@ +export = ChannelHeaderLinks; +declare class ChannelHeaderLinks { + constructor(item: any); + type: string; + primary: any; + secondary: any; +} diff --git a/typings/lib/parser/contents/ChannelMetadata.d.ts b/typings/lib/parser/contents/ChannelMetadata.d.ts index 8783c37b2..1de6ad2dd 100644 --- a/typings/lib/parser/contents/ChannelMetadata.d.ts +++ b/typings/lib/parser/contents/ChannelMetadata.d.ts @@ -4,18 +4,16 @@ declare class ChannelMetadata { type: string; title: any; description: any; - metadata: { - url: any; - rss_urls: any; - vanity_channel_url: any; - external_id: any; - is_family_safe: any; - keywords: any; - avatar: Thumbnail[]; - available_countries: any; - android_deep_link: any; - android_appindexing_link: any; - ios_appindexing_link: any; - }; + url: any; + rss_urls: any; + vanity_channel_url: any; + external_id: any; + is_family_safe: any; + keywords: any; + avatar: Thumbnail[]; + available_countries: any; + android_deep_link: any; + android_appindexing_link: any; + ios_appindexing_link: any; } import Thumbnail = require("./Thumbnail"); diff --git a/typings/lib/parser/contents/CompactVideo.d.ts b/typings/lib/parser/contents/CompactVideo.d.ts index 08289d9e0..3784c1b84 100644 --- a/typings/lib/parser/contents/CompactVideo.d.ts +++ b/typings/lib/parser/contents/CompactVideo.d.ts @@ -11,6 +11,7 @@ declare class CompactVideo { views: Text; duration: Text; endpoint: NavigationEndpoint; + get best_thumbnail(): Thumbnail; } import Thumbnail = require("./Thumbnail"); import Text = require("./Text"); diff --git a/typings/lib/parser/contents/ContinuationItem.d.ts b/typings/lib/parser/contents/ContinuationItem.d.ts index 84cb4eee1..2ec5f7ffe 100644 --- a/typings/lib/parser/contents/ContinuationItem.d.ts +++ b/typings/lib/parser/contents/ContinuationItem.d.ts @@ -10,6 +10,10 @@ declare class ContinuationItem { response: { contents: any; on_response_received_actions: any; + on_response_received_endpoints: any; + metadata: any; + header: any; + microformat: any; }; } import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/GridChannel.d.ts b/typings/lib/parser/contents/GridChannel.d.ts index 37ec0b278..fe681ef81 100644 --- a/typings/lib/parser/contents/GridChannel.d.ts +++ b/typings/lib/parser/contents/GridChannel.d.ts @@ -3,12 +3,11 @@ declare class GridChannel { constructor(item: any); type: string; id: any; - thumbnails: Thumbnail[]; - videos: Text; + author: Author; subscribers: Text; - name: Text; + videos: Text; endpoint: NavigationEndpoint; } -import Thumbnail = require("./Thumbnail"); +import Author = require("./Author"); import Text = require("./Text"); import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/GridPlaylist.d.ts b/typings/lib/parser/contents/GridPlaylist.d.ts index ee9ae3a99..a5f77b8c0 100644 --- a/typings/lib/parser/contents/GridPlaylist.d.ts +++ b/typings/lib/parser/contents/GridPlaylist.d.ts @@ -4,12 +4,13 @@ declare class GridPlaylist { type: string; id: any; videos: Text; - thumbnauls: Thumbnail[]; + thumbnails: Thumbnail[]; video_thumbnails: any; title: Text; endpoint: NavigationEndpoint; - view_playlist: Text; + view_playlist: NavigatableText; } import Text = require("./Text"); import Thumbnail = require("./Thumbnail"); import NavigationEndpoint = require("./NavigationEndpoint"); +import NavigatableText = require("./NavigatableText"); diff --git a/typings/lib/parser/contents/GridVideo.d.ts b/typings/lib/parser/contents/GridVideo.d.ts index 5c903350e..242be15db 100644 --- a/typings/lib/parser/contents/GridVideo.d.ts +++ b/typings/lib/parser/contents/GridVideo.d.ts @@ -3,6 +3,7 @@ declare class GridVideo { constructor(item: any); type: string; id: any; + author: Author; thumbnails: Thumbnail[]; rich_thumbnail: any; title: Text; @@ -11,7 +12,10 @@ declare class GridVideo { published_at: Text; views: Text; endpoint: NavigationEndpoint; + short_view_count: Text; + get best_thumbnail(): Thumbnail; } +import Author = require("./Author"); import Thumbnail = require("./Thumbnail"); import Text = require("./Text"); import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/MetadataBadge.d.ts b/typings/lib/parser/contents/MetadataBadge.d.ts index 6d1d72d7a..7b69f3c6d 100644 --- a/typings/lib/parser/contents/MetadataBadge.d.ts +++ b/typings/lib/parser/contents/MetadataBadge.d.ts @@ -5,4 +5,9 @@ declare class MetadataBadge { style: any; label: any; non_abbreviated_label: any; + /** + * Get label as string + * @returns {string} + */ + toString(): string; } diff --git a/typings/lib/parser/contents/MicroformatData.d.ts b/typings/lib/parser/contents/MicroformatData.d.ts new file mode 100644 index 000000000..48f80448a --- /dev/null +++ b/typings/lib/parser/contents/MicroformatData.d.ts @@ -0,0 +1,29 @@ +export = MicroformatData; +declare class MicroformatData { + constructor(item: any); + type: string; + url_canonical: any; + title: any; + description: any; + thumbnail: Thumbnail[]; + site_name: any; + app_name: any; + android_package: any; + ios_app_store_id: any; + ios_app_arguments: any; + og_type: any; + url_applinks_web: any; + url_applinks_ios: any; + url_applinks_android: any; + url_twitter_ios: any; + url_twitter_android: any; + twitter_card_type: any; + twitter_site_handle: any; + schema_dot_org_type: any; + noindex: any; + is_unlisted: any; + is_family_safe: any; + tags: any; + available_countries: any; +} +import Thumbnail = require("./Thumbnail"); diff --git a/typings/lib/parser/contents/NavigatableText.d.ts b/typings/lib/parser/contents/NavigatableText.d.ts index e957b7ebd..a5bedb5ad 100644 --- a/typings/lib/parser/contents/NavigatableText.d.ts +++ b/typings/lib/parser/contents/NavigatableText.d.ts @@ -2,7 +2,6 @@ export = NavigatableText; declare class NavigatableText extends Text { constructor(node: any); endpoint: NavigationEndpoint; - toString(): string; toJSON(): NavigatableText; } import Text = require("./Text"); diff --git a/typings/lib/parser/contents/NavigationEndpoint.d.ts b/typings/lib/parser/contents/NavigationEndpoint.d.ts index 47dfc2066..6200add38 100644 --- a/typings/lib/parser/contents/NavigationEndpoint.d.ts +++ b/typings/lib/parser/contents/NavigationEndpoint.d.ts @@ -1,4 +1,3 @@ -/// export = NavigationEndpoint; declare class NavigationEndpoint { constructor(item: any); @@ -33,7 +32,7 @@ declare class NavigationEndpoint { sequence_params: any; }; url: { - url: import("url").URL; + url: any; target: any; nofollow: any; }; @@ -48,5 +47,14 @@ declare class NavigationEndpoint { button: any; content: any; }; - call(session: any): any; + /** + * + * @param {import('../../Innertube')} session + * @returns + */ + call(session: import('../../Innertube')): Promise<{ + success: boolean; + status_code: number; + data: any; + }>; } diff --git a/typings/lib/parser/contents/Playlist.d.ts b/typings/lib/parser/contents/Playlist.d.ts index f4f35e956..e251e7b9e 100644 --- a/typings/lib/parser/contents/Playlist.d.ts +++ b/typings/lib/parser/contents/Playlist.d.ts @@ -9,8 +9,9 @@ declare class Playlist { videos: number; first_videos: any; endpoint: NavigationEndpoint; - view_playlist: Text; + view_playlist: NavigatableText; } import Text = require("./Text"); import Author = require("./Author"); import NavigationEndpoint = require("./NavigationEndpoint"); +import NavigatableText = require("./NavigatableText"); diff --git a/typings/lib/parser/contents/PlaylistPanelVideo.d.ts b/typings/lib/parser/contents/PlaylistPanelVideo.d.ts index 338b677dc..af830e88c 100644 --- a/typings/lib/parser/contents/PlaylistPanelVideo.d.ts +++ b/typings/lib/parser/contents/PlaylistPanelVideo.d.ts @@ -2,7 +2,7 @@ export = PlaylistPanelVideo; declare class PlaylistPanelVideo { constructor(item: any); type: string; - index: any; + index: string; selected: any; duration: Text; author: Author; @@ -10,6 +10,7 @@ declare class PlaylistPanelVideo { thumbnails: Thumbnail[]; title: Text; id: any; + get best_thumbnail(): Thumbnail; } import Text = require("./Text"); import Author = require("./Author"); diff --git a/typings/lib/parser/contents/PlaylistVideo.d.ts b/typings/lib/parser/contents/PlaylistVideo.d.ts index 3cd42964d..a561f0c83 100644 --- a/typings/lib/parser/contents/PlaylistVideo.d.ts +++ b/typings/lib/parser/contents/PlaylistVideo.d.ts @@ -2,16 +2,17 @@ export = PlaylistVideo; declare class PlaylistVideo { constructor(item: any); type: string; - index: any; + index: string; is_playable: any; duration: Text; endpoint: NavigationEndpoint; - author: NavigatableText; + author: Author; thumbnails: Thumbnail[]; title: Text; id: any; + get best_thumbnail(): Thumbnail; } import Text = require("./Text"); import NavigationEndpoint = require("./NavigationEndpoint"); -import NavigatableText = require("./NavigatableText"); +import Author = require("./Author"); import Thumbnail = require("./Thumbnail"); diff --git a/typings/lib/parser/contents/Shelf.d.ts b/typings/lib/parser/contents/Shelf.d.ts index 13dd1ded5..e163e9767 100644 --- a/typings/lib/parser/contents/Shelf.d.ts +++ b/typings/lib/parser/contents/Shelf.d.ts @@ -5,11 +5,7 @@ declare class Shelf { title: Text; content: any; endpoint: NavigationEndpoint; - button: { - text: Text; - endpoint: NavigationEndpoint; - icon: any; - }; + button: any; } import Text = require("./Text"); import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/Tab.d.ts b/typings/lib/parser/contents/Tab.d.ts index 3ef3720e7..5a0462be8 100644 --- a/typings/lib/parser/contents/Tab.d.ts +++ b/typings/lib/parser/contents/Tab.d.ts @@ -2,7 +2,10 @@ export = Tab; declare class Tab { constructor(item: any); type: string; - title: any; + /** + * @type {string} + */ + title: string; endpoint: NavigationEndpoint; selected: any; content: any; diff --git a/typings/lib/parser/contents/Text.d.ts b/typings/lib/parser/contents/Text.d.ts index 0d1faa874..a8f6291ff 100644 --- a/typings/lib/parser/contents/Text.d.ts +++ b/typings/lib/parser/contents/Text.d.ts @@ -2,11 +2,19 @@ export = Text; declare class Text { constructor(txt: any, def?: any); type: string; - text: any; + /** + * @type {string | undefined} + */ + text: string | undefined; runs: any; - toString(): any; + /** + * Get the string representation of this text + * @note may return an empty string if this.text is undefined + * @returns {string} + */ + toString(): string; toJSON(): { - text: any; + string: string; runs: any; }; } diff --git a/typings/lib/parser/contents/Thumbnail.d.ts b/typings/lib/parser/contents/Thumbnail.d.ts index 6635eb78f..bf3346ced 100644 --- a/typings/lib/parser/contents/Thumbnail.d.ts +++ b/typings/lib/parser/contents/Thumbnail.d.ts @@ -1,10 +1,10 @@ export = Thumbnail; declare class Thumbnail { /** - * Get thumbnails from response object - * @param {*} response response object - * @returns {Thumbnail[]} sorted array of thumbnails - */ + * Get thumbnails from response object + * @param {*} response response object + * @returns {Thumbnail[]} sorted array of thumbnails + */ static fromResponse({ thumbnails }: any): Thumbnail[]; constructor({ url, width, height }: { url: any; @@ -12,15 +12,15 @@ declare class Thumbnail { height: any; }); /** - * @type {string} - */ + * @type {string} + */ url: string; /** - * @type {number} - */ + * @type {number} + */ width: number; /** - * @type {number} - */ + * @type {number} + */ height: number; } diff --git a/typings/lib/parser/contents/index.d.ts b/typings/lib/parser/contents/index.d.ts index 734fc4e82..d43d8e87c 100644 --- a/typings/lib/parser/contents/index.d.ts +++ b/typings/lib/parser/contents/index.d.ts @@ -3,8 +3,12 @@ declare class ResultsParser { static parseResponse(data: any): { contents: any; on_response_received_actions: any; + on_response_received_endpoints: any; + metadata: any; + header: any; + microformat: any; }; - static parseRRA(actions: any): any; + static parseRR(actions: any): any; static parse(contents: any): any; static parseItem(item: any): any; } diff --git a/typings/lib/proto/index.d.ts b/typings/lib/proto/index.d.ts index 98c5547cf..b195e0012 100644 --- a/typings/lib/proto/index.d.ts +++ b/typings/lib/proto/index.d.ts @@ -1,7 +1,7 @@ export = Proto; declare class Proto { static encodeSearchFilter(period: any, duration: any, order: any): string; - static encodeMessageParams(channel_id: any, video_id: any): string; + static encodeMessageParams(channel_id: any, video_id: any): any; static encodeCommentsSectionParams(video_id: any, options?: {}): string; static encodeCommentRepliesParams(video_id: any, comment_id: any): string; static encodeCommentParams(video_id: any): string; diff --git a/typings/lib/utils/Request.d.ts b/typings/lib/utils/Request.d.ts index fc431022e..97b7f0331 100644 --- a/typings/lib/utils/Request.d.ts +++ b/typings/lib/utils/Request.d.ts @@ -2,6 +2,6 @@ export = Request; declare class Request { constructor(session: any); session: any; - instance: import("axios").AxiosInstance; + instance: any; #private; } From b100a1fa9978bfa24cbb5d5c3db17f032e430876 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sun, 1 May 2022 23:50:59 +0200 Subject: [PATCH 27/55] feat: initial playlist reimplementation --- lib/Innertube.js | 8 ++------ lib/core/Playlist.js | 15 +++++++++++++++ lib/parser/contents/MicroformatData.js | 2 +- lib/parser/contents/PlaylistMetadata.js | 13 +++++++++++++ .../contents/PlaylistSidebarPrimaryInfo.js | 18 ++++++++++++++++++ .../contents/PlaylistSidebarSecondaryInfo.js | 12 ++++++++++++ lib/parser/contents/PlaylistVideoThumbnail.js | 12 ++++++++++++ lib/parser/contents/VideoOwner.js | 3 +-- lib/parser/contents/index.js | 6 ++++++ 9 files changed, 80 insertions(+), 9 deletions(-) create mode 100644 lib/core/Playlist.js create mode 100644 lib/parser/contents/PlaylistMetadata.js create mode 100644 lib/parser/contents/PlaylistSidebarPrimaryInfo.js create mode 100644 lib/parser/contents/PlaylistSidebarSecondaryInfo.js create mode 100644 lib/parser/contents/PlaylistVideoThumbnail.js diff --git a/lib/Innertube.js b/lib/Innertube.js index 54d02528e..1e5b02bb0 100644 --- a/lib/Innertube.js +++ b/lib/Innertube.js @@ -21,6 +21,7 @@ const Signature = require('./deciphers/Signature'); const HomeFeed = require('./core/HomeFeed'); const { Trending } = require('./core/Trending'); const Channel = require('./core/Channel'); +const Playlist = require('./core/Playlist'); class Innertube { #oauth; @@ -664,12 +665,7 @@ class Innertube { const response = await this.actions.browse(`VL${playlist_id}`, { is_ytm: options.client == 'YTMUSIC' }); if (!response.success) throw new Utils.InnertubeError('Could not get playlist', response); - const playlist = new Parser(this, response.data, { - client: options.client, - data_type: 'PLAYLIST' - }).parse(); - - return playlist; + return new Playlist(this, response.data, options.client); } /** diff --git a/lib/core/Playlist.js b/lib/core/Playlist.js new file mode 100644 index 000000000..7670a7082 --- /dev/null +++ b/lib/core/Playlist.js @@ -0,0 +1,15 @@ +const Feed = require("./Feed"); + +class Playlist extends Feed { + #client; + constructor(session, data, client) { + super(session, data); + this.#client = client; + } + + async getContinuation() { + return new Playlist(this.session, await this.getContinuationData(), this.#client); + } +} + +module.exports = Playlist; diff --git a/lib/parser/contents/MicroformatData.js b/lib/parser/contents/MicroformatData.js index 89c73d1ea..38e1a713b 100644 --- a/lib/parser/contents/MicroformatData.js +++ b/lib/parser/contents/MicroformatData.js @@ -8,7 +8,7 @@ class MicroformatData { this.url_canonical = item.urlCanonical; this.title = item.title; this.description = item.description; - this.thumbnail = Thumbnail.fromResponse(item.thumbnail); + this.thumbnail = item.thumbnail && Thumbnail.fromResponse(item.thumbnail); this.site_name = item.siteName; this.app_name = item.appName; this.android_package = item.androidPackage; diff --git a/lib/parser/contents/PlaylistMetadata.js b/lib/parser/contents/PlaylistMetadata.js new file mode 100644 index 000000000..38cf2429a --- /dev/null +++ b/lib/parser/contents/PlaylistMetadata.js @@ -0,0 +1,13 @@ +const ResultsParser = require('.'); + +class PlaylistMetadata { + type = 'PlaylistMetadata'; + + constructor(item) { + this.title = item.title; + this.description = item.description; + // XXX: Appindexing should be in microformat + } +} + +module.exports = PlaylistMetadata; \ No newline at end of file diff --git a/lib/parser/contents/PlaylistSidebarPrimaryInfo.js b/lib/parser/contents/PlaylistSidebarPrimaryInfo.js new file mode 100644 index 000000000..336a68256 --- /dev/null +++ b/lib/parser/contents/PlaylistSidebarPrimaryInfo.js @@ -0,0 +1,18 @@ +const ResultsParser = require('.'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Text = require('./Text'); + +class PlaylistSidebarPrimaryInfo { + type = 'PlaylistSidebarPrimaryInfo'; + + constructor(item) { + this.stats = item.stats.map(stat => new Text(stat)); + this.thumbnail_renderer = ResultsParser.parseItem(item.thumbnailRenderer); + this.title = new Text(item.title); + this.menu = item.menu && ResultsParser.parseItem(item.menu); + this.endpoint = new NavigationEndpoint(item.navigationEndpoint); + this.description = new Text(item.description); + } +} + +module.exports = PlaylistSidebarPrimaryInfo; \ No newline at end of file diff --git a/lib/parser/contents/PlaylistSidebarSecondaryInfo.js b/lib/parser/contents/PlaylistSidebarSecondaryInfo.js new file mode 100644 index 000000000..7a18edae9 --- /dev/null +++ b/lib/parser/contents/PlaylistSidebarSecondaryInfo.js @@ -0,0 +1,12 @@ +const ResultsParser = require('.'); + +class PlaylistSidebarSecondaryInfo { + type = 'PlaylistSidebarSecondaryInfo'; + + constructor(item) { + this.owner = item.videoOwner && ResultsParser.parseItem(item.videoOwner); + this.button = item.button && ResultsParser.parseItem(item.button); + } +} + +module.exports = PlaylistSidebarSecondaryInfo; \ No newline at end of file diff --git a/lib/parser/contents/PlaylistVideoThumbnail.js b/lib/parser/contents/PlaylistVideoThumbnail.js new file mode 100644 index 000000000..49238f53c --- /dev/null +++ b/lib/parser/contents/PlaylistVideoThumbnail.js @@ -0,0 +1,12 @@ +const ResultsParser = require('.'); +const Thumbnail = require('./Thumbnail'); + +class PlaylistVideoThumbnail { + type = 'PlaylistVideoThumbnail'; + + constructor(item) { + this.thumbnail = Thumbnail.fromResponse(item.thumbnail); + } +} + +module.exports = PlaylistVideoThumbnail; \ No newline at end of file diff --git a/lib/parser/contents/VideoOwner.js b/lib/parser/contents/VideoOwner.js index c5d3d0221..6ddc4a574 100644 --- a/lib/parser/contents/VideoOwner.js +++ b/lib/parser/contents/VideoOwner.js @@ -5,11 +5,10 @@ class VideoOwner { type = 'VideoOwner'; constructor(item) { - /* this.author = new Author({ ...item.title, navigationEndpoint: item.navigationEndpoint - }, item.badges, item.thumbnail); */ + }, item.badges, item.thumbnail); } } diff --git a/lib/parser/contents/index.js b/lib/parser/contents/index.js index a969f7a50..69c19520a 100644 --- a/lib/parser/contents/index.js +++ b/lib/parser/contents/index.js @@ -16,6 +16,7 @@ class ResultsParser { header: data.header && ResultsParser.parseItem(data.header), // XXX: microformat contains meta tags for SEO? microformat: data.microformat && ResultsParser.parseItem(data.microformat), + sidebar: data.sidebar && ResultsParser.parseItem(data.sidebar), } } @@ -98,6 +99,11 @@ class ResultsParser { c4TabbedHeaderRenderer: require('./C4TabbedHeader'), microformatDataRenderer: require('./MicroformatData'), channelHeaderLinksRenderer: require('./ChannelHeaderLinks'), + playlistMetadataRenderer: require('./PlaylistMetadata'), + playlistSidebarRenderer: require('./GenericList')('PlaylistSidebar', 'items'), + playlistSidebarPrimaryInfoRenderer: require('./PlaylistSidebarPrimaryInfo'), + playlistVideoThumbnailRenderer: require('./PlaylistVideoThumbnail'), + playlistSidebarSecondaryInfoRenderer: require('./PlaylistSidebarSecondaryInfo'), } const keys = Reflect.ownKeys(item); From 79942b370d4369ebc79ddac3912b7a090081a758 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Sun, 1 May 2022 23:59:43 +0200 Subject: [PATCH 28/55] feat: complete playlist reimplementation --- lib/core/Playlist.js | 50 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/lib/core/Playlist.js b/lib/core/Playlist.js index 7670a7082..fd42efdc6 100644 --- a/lib/core/Playlist.js +++ b/lib/core/Playlist.js @@ -1,3 +1,4 @@ +const Simplify = require("../parser/simplify"); const Feed = require("./Feed"); class Playlist extends Feed { @@ -7,6 +8,55 @@ class Playlist extends Feed { this.#client = client; } + /** + * @returns {import('../parser/contents/PlaylistSidebarPrimaryInfo') | undefined} + */ + getPrimaryInfo() { + return Simplify.matching({ + type: Simplify.matching(/^PlaylistSidebarPrimaryInfo$/), + }).runOn(this.page)[0]; + } + + #getStat(index) { + const primaryInfo = this.getPrimaryInfo(); + if (!primaryInfo || !primaryInfo.stats) + return 'N/A'; + return primaryInfo.stats[index]?.toString() || 'N/A'; + } + + get title() { + const primaryInfo = this.getPrimaryInfo(); + if (!primaryInfo) + return ''; + return primaryInfo.title.toString(); + } + + get description() { + const primaryInfo = this.getPrimaryInfo(); + if (!primaryInfo) + return ''; + return primaryInfo.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; + } + async getContinuation() { return new Playlist(this.session, await this.getContinuationData(), this.#client); } From 4b74c99930ddaeff6827bff630b470aa1c849e97 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Mon, 2 May 2022 00:50:50 +0200 Subject: [PATCH 29/55] refactor: change InnertubeError to ES6 class --- lib/utils/Utils.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/utils/Utils.js b/lib/utils/Utils.js index 7a519bb17..356bb0e96 100644 --- a/lib/utils/Utils.js +++ b/lib/utils/Utils.js @@ -4,15 +4,13 @@ const Crypto = require('crypto'); const UserAgent = require('user-agents'); const Flatten = require('flat'); -function InnertubeError(message, info) { - this.info = info || {}; - this.stack = new Error(message).stack; - this.message = message; +class InnertubeError extends Error { + constructor(message, info) { + super(message); + this.info = info || {}; + } } -InnertubeError.prototype = Object.create(Error.prototype); -InnertubeError.prototype.constructor = InnertubeError; - class ParsingError extends InnertubeError {}; class DownloadError extends InnertubeError {}; class MissingParamError extends InnertubeError {}; From f6d90be8edf82d42d7467eed1496c741c488cba9 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Mon, 2 May 2022 00:51:18 +0200 Subject: [PATCH 30/55] fix: some unresolved types --- lib/Innertube.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Innertube.js b/lib/Innertube.js index 1e5b02bb0..a555387ac 100644 --- a/lib/Innertube.js +++ b/lib/Innertube.js @@ -765,7 +765,7 @@ class Innertube { * @param {string} options.type - download type, can be: video, audio or videoandaudio * @param {string} options.format - file format * - * @return {ReadableStream} + * @return {Stream.PassThrough} */ download(id, options = {}) { if (!id) throw new Utils.MissingParamError('Video id is missing'); From dcfa563553ba224fbf78f1dd0a21165bd080b28a Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Mon, 2 May 2022 00:52:31 +0200 Subject: [PATCH 31/55] chore: update types --- typings/lib/Innertube.d.ts | 6 ++++-- typings/lib/core/Channel.d.ts | 5 +++++ typings/lib/core/Livechat.d.ts | 5 +++-- typings/lib/core/Playlist.d.ts | 20 +++++++++++++++++++ typings/lib/core/Trending.d.ts | 1 + typings/lib/deciphers/Signature.d.ts | 2 +- .../lib/parser/contents/ContinuationItem.d.ts | 1 + .../parser/contents/NavigationEndpoint.d.ts | 3 ++- .../lib/parser/contents/PlaylistMetadata.d.ts | 7 +++++++ .../contents/PlaylistSidebarPrimaryInfo.d.ts | 13 ++++++++++++ .../PlaylistSidebarSecondaryInfo.d.ts | 7 +++++++ .../contents/PlaylistVideoThumbnail.d.ts | 7 +++++++ typings/lib/parser/contents/VideoOwner.d.ts | 2 ++ typings/lib/parser/contents/index.d.ts | 1 + typings/lib/proto/index.d.ts | 2 +- typings/lib/utils/Utils.d.ts | 6 +----- 16 files changed, 76 insertions(+), 12 deletions(-) create mode 100644 typings/lib/core/Playlist.d.ts create mode 100644 typings/lib/parser/contents/PlaylistMetadata.d.ts create mode 100644 typings/lib/parser/contents/PlaylistSidebarPrimaryInfo.d.ts create mode 100644 typings/lib/parser/contents/PlaylistSidebarSecondaryInfo.d.ts create mode 100644 typings/lib/parser/contents/PlaylistVideoThumbnail.d.ts diff --git a/typings/lib/Innertube.d.ts b/typings/lib/Innertube.d.ts index ef51fec97..7af9d0450 100644 --- a/typings/lib/Innertube.d.ts +++ b/typings/lib/Innertube.d.ts @@ -483,17 +483,19 @@ declare class Innertube { * @param {string} options.type - download type, can be: video, audio or videoandaudio * @param {string} options.format - file format * - * @return {ReadableStream} + * @return {Stream.PassThrough} */ download(id: string, options?: { quality: string; type: string; format: string; - }): ReadableStream; + }): Stream.PassThrough; #private; } +import EventEmitter = require("events"); import Request = require("./utils/Request"); import Actions = require("./core/Actions"); import Channel = require("./core/Channel"); import HomeFeed = require("./core/HomeFeed"); import { Trending } from "./core/Trending"; +import Stream = require("stream"); diff --git a/typings/lib/core/Channel.d.ts b/typings/lib/core/Channel.d.ts index 7a93952da..0d056f587 100644 --- a/typings/lib/core/Channel.d.ts +++ b/typings/lib/core/Channel.d.ts @@ -8,6 +8,11 @@ declare class Channel { metadata: any; header: any; microformat: any; + sidebar: any; /** + * + * @param {string} name + * @returns {import('../parser/contents/Tab')} + */ }; get title(): any; get description(): any; diff --git a/typings/lib/core/Livechat.d.ts b/typings/lib/core/Livechat.d.ts index a054b698b..f709737f9 100644 --- a/typings/lib/core/Livechat.d.ts +++ b/typings/lib/core/Livechat.d.ts @@ -1,5 +1,5 @@ export = Livechat; -declare class Livechat { +declare class Livechat extends EventEmitter { constructor(session: any, token: any, channel_id: any, video_id: any); ctoken: any; session: any; @@ -10,7 +10,7 @@ declare class Livechat { poll_intervals_ms: number; running: boolean; metadata_ctoken: any; - livechat_poller: any; + livechat_poller: NodeJS.Timeout; sendMessage(text: any): Promise; /** * Blocks a user. @@ -21,3 +21,4 @@ declare class Livechat { stop(): void; #private; } +import EventEmitter = require("events"); diff --git a/typings/lib/core/Playlist.d.ts b/typings/lib/core/Playlist.d.ts new file mode 100644 index 000000000..bf52ad491 --- /dev/null +++ b/typings/lib/core/Playlist.d.ts @@ -0,0 +1,20 @@ +export = Playlist; +declare class Playlist extends Feed { + constructor(session: any, data: any, client: any); + /** + * @returns {import('../parser/contents/PlaylistSidebarPrimaryInfo') | undefined} + */ + getPrimaryInfo(): import('../parser/contents/PlaylistSidebarPrimaryInfo') | undefined; + get title(): string; + get description(): string; + get total_items(): any; + get views(): any; + get last_updated(): any; + /** + * @alias videos + */ + get items(): import("./SimpleVideo")[]; + getContinuation(): Promise; + #private; +} +import Feed = require("./Feed"); diff --git a/typings/lib/core/Trending.d.ts b/typings/lib/core/Trending.d.ts index 192d028c7..cd134b2d1 100644 --- a/typings/lib/core/Trending.d.ts +++ b/typings/lib/core/Trending.d.ts @@ -21,6 +21,7 @@ export class Trending { metadata: any; header: any; microformat: any; + sidebar: any; }; #private; } diff --git a/typings/lib/deciphers/Signature.d.ts b/typings/lib/deciphers/Signature.d.ts index cb737e13e..65fa00599 100644 --- a/typings/lib/deciphers/Signature.d.ts +++ b/typings/lib/deciphers/Signature.d.ts @@ -8,6 +8,6 @@ declare class Signature { /** * Deciphers signature. */ - decipher(): any; + decipher(): string; #private; } diff --git a/typings/lib/parser/contents/ContinuationItem.d.ts b/typings/lib/parser/contents/ContinuationItem.d.ts index 2ec5f7ffe..fe635b4be 100644 --- a/typings/lib/parser/contents/ContinuationItem.d.ts +++ b/typings/lib/parser/contents/ContinuationItem.d.ts @@ -14,6 +14,7 @@ declare class ContinuationItem { metadata: any; header: any; microformat: any; + sidebar: any; }; } import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/NavigationEndpoint.d.ts b/typings/lib/parser/contents/NavigationEndpoint.d.ts index 6200add38..75cc1f54c 100644 --- a/typings/lib/parser/contents/NavigationEndpoint.d.ts +++ b/typings/lib/parser/contents/NavigationEndpoint.d.ts @@ -1,3 +1,4 @@ +/// export = NavigationEndpoint; declare class NavigationEndpoint { constructor(item: any); @@ -32,7 +33,7 @@ declare class NavigationEndpoint { sequence_params: any; }; url: { - url: any; + url: import("url").URL; target: any; nofollow: any; }; diff --git a/typings/lib/parser/contents/PlaylistMetadata.d.ts b/typings/lib/parser/contents/PlaylistMetadata.d.ts new file mode 100644 index 000000000..9e86ce1a6 --- /dev/null +++ b/typings/lib/parser/contents/PlaylistMetadata.d.ts @@ -0,0 +1,7 @@ +export = PlaylistMetadata; +declare class PlaylistMetadata { + constructor(item: any); + type: string; + title: any; + description: any; +} diff --git a/typings/lib/parser/contents/PlaylistSidebarPrimaryInfo.d.ts b/typings/lib/parser/contents/PlaylistSidebarPrimaryInfo.d.ts new file mode 100644 index 000000000..a9e46708e --- /dev/null +++ b/typings/lib/parser/contents/PlaylistSidebarPrimaryInfo.d.ts @@ -0,0 +1,13 @@ +export = PlaylistSidebarPrimaryInfo; +declare class PlaylistSidebarPrimaryInfo { + constructor(item: any); + type: string; + stats: any; + thumbnail_renderer: any; + title: Text; + menu: any; + endpoint: NavigationEndpoint; + description: Text; +} +import Text = require("./Text"); +import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/PlaylistSidebarSecondaryInfo.d.ts b/typings/lib/parser/contents/PlaylistSidebarSecondaryInfo.d.ts new file mode 100644 index 000000000..2c15bef91 --- /dev/null +++ b/typings/lib/parser/contents/PlaylistSidebarSecondaryInfo.d.ts @@ -0,0 +1,7 @@ +export = PlaylistSidebarSecondaryInfo; +declare class PlaylistSidebarSecondaryInfo { + constructor(item: any); + type: string; + owner: any; + button: any; +} diff --git a/typings/lib/parser/contents/PlaylistVideoThumbnail.d.ts b/typings/lib/parser/contents/PlaylistVideoThumbnail.d.ts new file mode 100644 index 000000000..20297c3ef --- /dev/null +++ b/typings/lib/parser/contents/PlaylistVideoThumbnail.d.ts @@ -0,0 +1,7 @@ +export = PlaylistVideoThumbnail; +declare class PlaylistVideoThumbnail { + constructor(item: any); + type: string; + thumbnail: Thumbnail[]; +} +import Thumbnail = require("./Thumbnail"); diff --git a/typings/lib/parser/contents/VideoOwner.d.ts b/typings/lib/parser/contents/VideoOwner.d.ts index 400f4dcb7..663335c05 100644 --- a/typings/lib/parser/contents/VideoOwner.d.ts +++ b/typings/lib/parser/contents/VideoOwner.d.ts @@ -2,4 +2,6 @@ export = VideoOwner; declare class VideoOwner { constructor(item: any); type: string; + author: Author; } +import Author = require("./Author"); diff --git a/typings/lib/parser/contents/index.d.ts b/typings/lib/parser/contents/index.d.ts index d43d8e87c..bc80b98d5 100644 --- a/typings/lib/parser/contents/index.d.ts +++ b/typings/lib/parser/contents/index.d.ts @@ -7,6 +7,7 @@ declare class ResultsParser { metadata: any; header: any; microformat: any; + sidebar: any; }; static parseRR(actions: any): any; static parse(contents: any): any; diff --git a/typings/lib/proto/index.d.ts b/typings/lib/proto/index.d.ts index b195e0012..98c5547cf 100644 --- a/typings/lib/proto/index.d.ts +++ b/typings/lib/proto/index.d.ts @@ -1,7 +1,7 @@ export = Proto; declare class Proto { static encodeSearchFilter(period: any, duration: any, order: any): string; - static encodeMessageParams(channel_id: any, video_id: any): any; + static encodeMessageParams(channel_id: any, video_id: any): string; static encodeCommentsSectionParams(video_id: any, options?: {}): string; static encodeCommentRepliesParams(video_id: any, comment_id: any): string; static encodeCommentParams(video_id: any): string; diff --git a/typings/lib/utils/Utils.d.ts b/typings/lib/utils/Utils.d.ts index 2c4650abe..ed310208c 100644 --- a/typings/lib/utils/Utils.d.ts +++ b/typings/lib/utils/Utils.d.ts @@ -4,13 +4,9 @@ export class ParsingError extends InnertubeError { } export class DownloadError extends InnertubeError { } -export function InnertubeError(message: any, info: any): void; -export class InnertubeError { +export class InnertubeError extends Error { constructor(message: any, info: any); info: any; - stack: string; - message: any; - constructor: typeof InnertubeError; } export class MissingParamError extends InnertubeError { } From 1dd123d6621c3309d9e3bb8b62361b8af12aa40c Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Thu, 5 May 2022 06:28:38 +0200 Subject: [PATCH 32/55] feat: wip video details --- lib/Innertube.js | 18 +++++ lib/core/Channel.js | 2 +- lib/core/Feed.js | 4 +- .../{SimplePlaylist.js => PlaylistItem.js} | 2 + lib/core/Trending.js | 2 +- lib/core/Video.js | 81 +++++++++++++++++++ lib/core/{SimpleVideo.js => VideoItem.js} | 2 + lib/parser/contents/Format.js | 61 ++++++++++++++ lib/parser/contents/NavigationEndpoint.js | 8 +- lib/parser/contents/PlayerStoryboardSpec.js | 55 +++++++++++++ lib/parser/contents/VideoDetails.js | 47 +++++++++++ lib/parser/contents/index.js | 36 +++++++++ package.json | 5 +- typings/lib/Innertube.d.ts | 8 ++ typings/lib/core/Channel.d.ts | 21 +++-- typings/lib/core/Feed.d.ts | 4 +- typings/lib/core/Playlist.d.ts | 2 +- typings/lib/core/PlaylistItem.d.ts | 25 ++++++ typings/lib/core/Trending.d.ts | 17 +++- typings/lib/core/Video.d.ts | 45 +++++++++++ typings/lib/core/VideoItem.d.ts | 43 ++++++++++ .../lib/parser/contents/ContinuationItem.d.ts | 15 ++++ typings/lib/parser/contents/Format.d.ts | 35 ++++++++ .../parser/contents/NavigationEndpoint.d.ts | 4 + .../parser/contents/PlayerStoryboardSpec.d.ts | 11 +++ typings/lib/parser/contents/VideoDetails.d.ts | 37 +++++++++ typings/lib/parser/contents/index.d.ts | 23 ++++++ 27 files changed, 597 insertions(+), 16 deletions(-) rename lib/core/{SimplePlaylist.js => PlaylistItem.js} (96%) create mode 100644 lib/core/Video.js rename lib/core/{SimpleVideo.js => VideoItem.js} (98%) create mode 100644 lib/parser/contents/Format.js create mode 100644 lib/parser/contents/PlayerStoryboardSpec.js create mode 100644 lib/parser/contents/VideoDetails.js create mode 100644 typings/lib/core/PlaylistItem.d.ts create mode 100644 typings/lib/core/Video.d.ts create mode 100644 typings/lib/core/VideoItem.d.ts create mode 100644 typings/lib/parser/contents/Format.d.ts create mode 100644 typings/lib/parser/contents/PlayerStoryboardSpec.d.ts create mode 100644 typings/lib/parser/contents/VideoDetails.d.ts diff --git a/lib/Innertube.js b/lib/Innertube.js index a555387ac..0614685f2 100644 --- a/lib/Innertube.js +++ b/lib/Innertube.js @@ -22,6 +22,7 @@ const HomeFeed = require('./core/HomeFeed'); const { Trending } = require('./core/Trending'); const Channel = require('./core/Channel'); const Playlist = require('./core/Playlist'); +const Video = require('./core/Video'); class Innertube { #oauth; @@ -502,6 +503,19 @@ class Innertube { return details; } + /** + * This is temorary, will replace getDetails() in the future. + * @param {*} video_id + * @returns + */ + async _getDetails(video_id) { + if (!video_id) throw new Utils.MissingParamError('Video id is missing'); + + const response = await this.actions.getVideoInfo(video_id); + + return new Video(this, response); + } + /** * Retrieves comments for a video. * @@ -905,6 +919,10 @@ class Innertube { return stream; } + + getPlayer() { + return this.#player; + } } module.exports = Innertube; \ No newline at end of file diff --git a/lib/core/Channel.js b/lib/core/Channel.js index 6f433fad4..432f7ca0e 100644 --- a/lib/core/Channel.js +++ b/lib/core/Channel.js @@ -2,7 +2,7 @@ const ResultsParser = require("../parser/contents"); const Simplify = require("../parser/simplify"); const { InnertubeError } = require("../utils/Utils"); const Feed = require("./Feed"); -const SimpleVideo = require("./SimpleVideo"); +const SimpleVideo = require("./VideoItem"); class Channel { #page; diff --git a/lib/core/Feed.js b/lib/core/Feed.js index c8e1a2141..b0853813f 100644 --- a/lib/core/Feed.js +++ b/lib/core/Feed.js @@ -1,8 +1,8 @@ const ResultsParser = require('../parser/contents'); const Simplify = require('../parser/simplify'); const { InnertubeError } = require('../utils/Utils'); -const SimplePlaylist = require('./SimplePlaylist'); -const SimpleVideo = require('./SimpleVideo'); +const SimplePlaylist = require('./PlaylistItem'); +const SimpleVideo = require('./VideoItem'); // TODO: add a way subdivide into sections and return subfeeds? diff --git a/lib/core/SimplePlaylist.js b/lib/core/PlaylistItem.js similarity index 96% rename from lib/core/SimplePlaylist.js rename to lib/core/PlaylistItem.js index bc0f697e2..547e9bbc3 100644 --- a/lib/core/SimplePlaylist.js +++ b/lib/core/PlaylistItem.js @@ -4,6 +4,8 @@ * - GridPlaylist * * Provides a unified interface for all of them. + * + * TODO: refactor - name this PlaylistItem */ class SimplePlaylist { /** diff --git a/lib/core/Trending.js b/lib/core/Trending.js index 62829e594..b674dbb27 100644 --- a/lib/core/Trending.js +++ b/lib/core/Trending.js @@ -1,7 +1,7 @@ const ResultsParser = require("../parser/contents"); const Simplify = require("../parser/simplify"); const { InnertubeError } = require("../utils/Utils"); -const SimpleVideo = require("./SimpleVideo"); +const SimpleVideo = require("./VideoItem"); class TrendingTab { /** diff --git a/lib/core/Video.js b/lib/core/Video.js new file mode 100644 index 000000000..477bc66bc --- /dev/null +++ b/lib/core/Video.js @@ -0,0 +1,81 @@ +const ResultsParser = require("../parser/contents"); + +class Video { + #page; + #session; + constructor (session, page) { + this.#page = ResultsParser.parseResponse(page); + this.#session = session; + } + + get id() { + return this.#page.video_details.id; + } + + get title() { + return this.#page.video_details.title; + } + + get description() { + return this.#page.video_details.short_description; + } + + get thumbnail() { + return this.#page.video_details.thumbnail[0]; + } + + get metadata() { + // TODO: this is in playerMicroformatRenderer + } + + get captions() { + // TODO: playerCaptionsTracklistRenderer + } + + get storyboards() { + // TODO: + } + + // THIS IS TEMPORARY: + getFormatURL() { + return this.#page.streaming_data.adaptive_formats[0].getDecipheredUrl(this.#session); + } + + like() { + + } + + dislike() { + + } + + removeLike() { + + } + + unsubscribe() { + + } + + comment() { + + } + + getComments() { + + } + + getLivechat() { + + } + + setNotificationPreferences() { + + } + + get page() { + return this.#page; + } +} + +module.exports = Video; diff --git a/lib/core/SimpleVideo.js b/lib/core/VideoItem.js similarity index 98% rename from lib/core/SimpleVideo.js rename to lib/core/VideoItem.js index 9312a87f8..9daf3f7e7 100644 --- a/lib/core/SimpleVideo.js +++ b/lib/core/VideoItem.js @@ -10,6 +10,8 @@ const Utils = require('../utils/Utils'); * - TODO: WatchCardCompactVideo * * Provides a unified interface for all of them. + * + * TODO: refactor - name this VideoItem */ class SimpleVideo { /** diff --git a/lib/parser/contents/Format.js b/lib/parser/contents/Format.js new file mode 100644 index 000000000..49fba7433 --- /dev/null +++ b/lib/parser/contents/Format.js @@ -0,0 +1,61 @@ +const NToken = require("../../deciphers/NToken"); +const Signature = require("../../deciphers/Signature"); + +class Format { + constructor(item) { + this.itag = item.itag; + this.mime_type = item.mimeType; + this.bitrate = item.bitrate; + this.width = item.width; + this.height = item.height; + this.last_modified = new Date(Math.floor(parseInt(item.lastModified) / 1000)); + this.content_length = parseInt(item.contentLength); + this.quality = item.quality; + this.fps = item.fps; + this.quality_label = item.qualityLabel; + this.projection_type = item.projectionType; + this.average_bitrate = item.averageBitrate; + this.audio_bitrate = item.audioBitrate; + this.audio_quality = item.audioQuality; + this.approx_duration_ms = parseInt(item.approxDurationMs); + this.audio_sample_rate = parseInt(item.audioSampleRate); + this.audio_channels = item.audioChannels; + this.signature_cipher = item.signatureCipher; + this.cipher = item.cipher; + this._url = item.url; + this.init_range = item.initRange && { + start: parseInt(item.initRange.start), + end: parseInt(item.initRange.end) + }; + this.index_range = item.indexRange && { + start: parseInt(item.indexRange.start), + end: parseInt(item.indexRange.end) + }; + } + + get has_audio() { + return !!this.audio_bitrate || !!this.audio_quality; + } + + get has_video() { + return !!this.quality_label; + } + + getDecipheredUrl(session) { + let url = this._url || this.signature_cipher || this.cipher; + if (this.signature_cipher || this.cipher) { + url = new Signature(url, session.getPlayer()).decipher(); + } + const url_components = new URL(url); + url_components.searchParams.set('cver', session.context.client.clientVersion); + url_components.searchParams.set('ratebypass', 'yes'); + + if (url_components.searchParams.get('n')) { + url_components.searchParams.set('n', new NToken(session.getPlayer().ntoken_sc, url_components.searchParams.get('n')).transform()); + } + + return url_components.toString(); + } +} + +module.exports = Format; diff --git a/lib/parser/contents/NavigationEndpoint.js b/lib/parser/contents/NavigationEndpoint.js index 37660953d..bfe267885 100644 --- a/lib/parser/contents/NavigationEndpoint.js +++ b/lib/parser/contents/NavigationEndpoint.js @@ -56,7 +56,9 @@ class NavigationEndpoint { } : null; this.is_reveal_business_emal = !!item.revealBusinessEmailCommand; // TODO: sign in endpoints - + this.sign_in = item.signInEndpoint ? { + next: item.signInEndpoint.nextEndpoint && new NavigationEndpoint(item.signInEndpoint.nextEndpoint) + } : null; // TODO: modal endpoints cleanup const modalRenderer = item.modalEndpoint?.moadlWithTitleAndButtonRenderer; this.modal = modalRenderer ? { @@ -64,6 +66,10 @@ class NavigationEndpoint { button: ResultsParser.parseItem(modalRenderer.button), content: modalRenderer.content, } : null; + // TODO: perform_comment_action + this.perform_comment_action = item.performCommentActionEndpoint ? { + + } : null; } /** diff --git a/lib/parser/contents/PlayerStoryboardSpec.js b/lib/parser/contents/PlayerStoryboardSpec.js new file mode 100644 index 000000000..0133a4738 --- /dev/null +++ b/lib/parser/contents/PlayerStoryboardSpec.js @@ -0,0 +1,55 @@ +const ResultsParser = require('.'); + +/** + * This is taken directly from node-ytdl-core + * which is under the same MIT license as this library. + * @see https://github.com/fent/node-ytdl-core/blob/11103be3ab373551bddd0938bc0bbaea033acb8e/lib/info-extras.js#L297 + */ + +class PlayerStoryboardSpec { + type = 'PlayerStoryboardSpec'; + + constructor(item) { + const parts = item.spec?.split('|'); + + if (!parts) { + this.boards = []; + } + + const url = new URL(parts.shift()); + + this.boards = parts.map((part, i) => { + let [ + thumbnail_width, + thumbnail_height, + thumbnail_count, + columns, + rows, + interval, + nameReplacement, + sigh, + ] = part.split('#'); + + url.searchParams.set('sigh', sigh); + + thumbnail_count = parseInt(thumbnail_count, 10); + columns = parseInt(columns, 10); + rows = parseInt(rows, 10); + + const storyboard_count = Math.ceil(thumbnail_count / (columns * rows)); + + return { + template_url: url.toString().replace('$L', i).replace('$N', nameReplacement), + thumbnail_width: parseInt(thumbnail_width, 10), + thumbnail_height: parseInt(thumbnail_height, 10), + thumbnail_count, + interval: parseInt(interval, 10), + columns, + rows, + storyboard_count, + }; + }); + } +} + +module.exports = PlayerStoryboardSpec; \ No newline at end of file diff --git a/lib/parser/contents/VideoDetails.js b/lib/parser/contents/VideoDetails.js new file mode 100644 index 000000000..b91daac19 --- /dev/null +++ b/lib/parser/contents/VideoDetails.js @@ -0,0 +1,47 @@ +const Thumbnail = require("./Thumbnail"); + +class VideoDetails { + /** + * @type {string} + */ + id; + /** + * @type {string} + */ + channel_id; + /** + * @type {string} + */ + title; + /** + * @type {string[]} + */ + keywords; + /** + * @type {string} + */ + short_description; + /** + * @type {string} + */ + author; + + constructor(item) { + this.id = item.videoId; + this.channel_id = item.channelId; + this.title = item.title; + this.duration = parseInt(item.lengthSeconds); + this.keywords = item.keywords; + this.is_owner_viewing = !!item.isOwnerViewing; + this.short_description = item.shortDescription; + this.thumbnail = Thumbnail.fromResponse(item.thumbnail); + this.allow_ratings = !!item.allowRatings; + this.view_count = parseInt(item.viewCount); + this.author = item.author; + this.is_private = !!item.isPrivate; + this.is_live_content = !!item.isLiveContent; + this.is_crawlable = !!item.isCrawlable; + } +} + +module.exports = VideoDetails; diff --git a/lib/parser/contents/index.js b/lib/parser/contents/index.js index 69c19520a..4db2d0047 100644 --- a/lib/parser/contents/index.js +++ b/lib/parser/contents/index.js @@ -1,3 +1,6 @@ +const Format = require('./Format'); +const VideoDetails = require('./VideoDetails'); + class AppendContinuationItemsAction { type = 'AppendContinuationItemsAction'; @@ -17,9 +20,41 @@ class ResultsParser { // XXX: microformat contains meta tags for SEO? microformat: data.microformat && ResultsParser.parseItem(data.microformat), sidebar: data.sidebar && ResultsParser.parseItem(data.sidebar), + + + // these are specifically for player + playability_status: data.playabilityStatus && { + status: data.playabilityStatus.status, + embeddable: data.playabilityStatus.playableInEmbed, + }, + streaming_data: data.streamingData && { + expires: new Date(Date.now() + parseInt(data.streamingData.expiresInSeconds) * 1000), + formats: ResultsParser.parseFormats(data.streamingData.formats), + adaptive_formats: ResultsParser.parseFormats(data.streamingData.adaptiveFormats), + }, + // XXX: ADS? do we want this? + // player_ads: data.playerAds && + + captions: data.captions && ResultsParser.parseItem(data.captions), + video_details: data.videoDetails && new VideoDetails(data.videoDetails), + annotations: data.annotations && ResultsParser.parse(data.annotations), + // TODO: playerConfig: this might be useful? + storyboards: data.storyboards && ResultsParser.parseItem(data.storyboards), + endscreen: data.endscreen && ResultsParser.parseItem(data.endscreen), + // cards are those lil icons on the top right of the video + cards: data.cards && ResultsParser.parseItem(data.cards), } } + /** + * + * @param {*} formats + * @returns {Format[]} + */ + static parseFormats(formats) { + return formats.map(format => new Format(format)); + } + static parseRR(actions) { return actions.map((action) => { if (Object.keys(action).includes('appendContinuationItemsAction')) @@ -104,6 +139,7 @@ class ResultsParser { playlistSidebarPrimaryInfoRenderer: require('./PlaylistSidebarPrimaryInfo'), playlistVideoThumbnailRenderer: require('./PlaylistVideoThumbnail'), playlistSidebarSecondaryInfoRenderer: require('./PlaylistSidebarSecondaryInfo'), + playerStoryboardSpecRenderer: require('./PlayerStoryboardSpec'), } const keys = Reflect.ownKeys(item); diff --git a/package.json b/package.json index d746c9efc..617acb5c4 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "node": ">=14" }, "scripts": { - "test": "jest", - "types": "npx tsc" + "test": "node test", + "build:types": "npx tsc" }, "types": "./typings/index.d.ts", "directories": { @@ -55,6 +55,7 @@ "dl" ], "devDependencies": { + "@types/node": "^17.0.31", "typescript": "^4.6.4" } } diff --git a/typings/lib/Innertube.d.ts b/typings/lib/Innertube.d.ts index 7af9d0450..fe6f6ec19 100644 --- a/typings/lib/Innertube.d.ts +++ b/typings/lib/Innertube.d.ts @@ -348,6 +348,12 @@ declare class Innertube { thumbnail: []; metadata: object; }>; + /** + * This is temorary, will replace getDetails() in the future. + * @param {*} video_id + * @returns + */ + _getDetails(video_id: any): Promise