From a352ddeb9db001e99f49025048ad0942d84f1b3e Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Wed, 24 Jul 2024 20:53:27 +0200 Subject: [PATCH 01/18] feat(Format): Add `is_secondary` for detecting secondary audio tracks (#697) --- src/parser/classes/misc/Format.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/parser/classes/misc/Format.ts b/src/parser/classes/misc/Format.ts index 9c199948b..58578e22f 100644 --- a/src/parser/classes/misc/Format.ts +++ b/src/parser/classes/misc/Format.ts @@ -57,6 +57,7 @@ export default class Format { language?: string | null; is_dubbed?: boolean; is_descriptive?: boolean; + is_secondary?: boolean; is_original?: boolean; color_info?: { primaries?: string; @@ -213,7 +214,8 @@ export default class Format { const audio_content = xtags?.find((x) => x.startsWith('acont='))?.split('=')[1]; this.is_dubbed = audio_content === 'dubbed'; this.is_descriptive = audio_content === 'descriptive'; - this.is_original = audio_content === 'original' || (!this.is_dubbed && !this.is_descriptive && !this.is_drc); + this.is_secondary = audio_content === 'secondary'; + this.is_original = audio_content === 'original' || (!this.is_dubbed && !this.is_descriptive && !this.is_secondary && !this.is_drc); } // Some text tracks don't have xtags while others do From a5f62093a18705fc822abd86beaa81788b6535ce Mon Sep 17 00:00:00 2001 From: GurumNyang Date: Thu, 25 Jul 2024 03:54:14 +0900 Subject: [PATCH 02/18] feature(proto): Add `comment_id` to commentSectionParams (#693) --- .../(Params)/Options.ts | 17 +++++++++++++++++ src/proto/index.ts | 6 ++++-- src/proto/youtube.proto | 1 + 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/proto/generated/messages/youtube/(GetCommentsSectionParams)/(Params)/Options.ts b/src/proto/generated/messages/youtube/(GetCommentsSectionParams)/(Params)/Options.ts index 582cd07eb..bc4928d78 100644 --- a/src/proto/generated/messages/youtube/(GetCommentsSectionParams)/(Params)/Options.ts +++ b/src/proto/generated/messages/youtube/(GetCommentsSectionParams)/(Params)/Options.ts @@ -21,6 +21,7 @@ export declare namespace $.youtube.GetCommentsSectionParams.Params { videoId: string; sortBy: number; type: number; + commentId?: string; } } @@ -31,6 +32,7 @@ export function getDefaultValue(): $.youtube.GetCommentsSectionParams.Params.Opt videoId: "", sortBy: 0, type: 0, + commentId: undefined, }; } @@ -46,6 +48,7 @@ export function encodeJson(value: $.youtube.GetCommentsSectionParams.Params.Opti if (value.videoId !== undefined) result.videoId = tsValueToJsonValueFns.string(value.videoId); if (value.sortBy !== undefined) result.sortBy = tsValueToJsonValueFns.int32(value.sortBy); if (value.type !== undefined) result.type = tsValueToJsonValueFns.int32(value.type); + if (value.commentId !== undefined) result.commentId = tsValueToJsonValueFns.string(value.commentId); return result; } @@ -54,6 +57,7 @@ export function decodeJson(value: any): $.youtube.GetCommentsSectionParams.Param if (value.videoId !== undefined) result.videoId = jsonValueToTsValueFns.string(value.videoId); if (value.sortBy !== undefined) result.sortBy = jsonValueToTsValueFns.int32(value.sortBy); if (value.type !== undefined) result.type = jsonValueToTsValueFns.int32(value.type); + if (value.commentId !== undefined) result.commentId = jsonValueToTsValueFns.string(value.commentId); return result; } @@ -77,6 +81,12 @@ export function encodeBinary(value: $.youtube.GetCommentsSectionParams.Params.Op [15, tsValueToWireValueFns.int32(tsValue)], ); } + if (value.commentId !== undefined) { + const tsValue = value.commentId; + result.push( + [16, tsValueToWireValueFns.string(tsValue)], + ); + } return serialize(result); } @@ -105,5 +115,12 @@ export function decodeBinary(binary: Uint8Array): $.youtube.GetCommentsSectionPa if (value === undefined) break field; result.type = value; } + field: { + const wireValue = wireFields.get(16); + if (wireValue === undefined) break field; + const value = wireValueToTsValueFns.string(wireValue); + if (value === undefined) break field; + result.commentId = value; + } return result; } diff --git a/src/proto/index.ts b/src/proto/index.ts index 8f72a9042..eee1aa110 100644 --- a/src/proto/index.ts +++ b/src/proto/index.ts @@ -155,7 +155,8 @@ export function encodeMessageParams(channel_id: string, video_id: string): strin export function encodeCommentsSectionParams(video_id: string, options: { type?: number, - sort_by?: 'TOP_COMMENTS' | 'NEWEST_FIRST' + sort_by?: 'TOP_COMMENTS' | 'NEWEST_FIRST', + comment_id?: string } = {}): string { const sort_options = { TOP_COMMENTS: 0, @@ -171,7 +172,8 @@ export function encodeCommentsSectionParams(video_id: string, options: { opts: { videoId: video_id, sortBy: sort_options[options.sort_by || 'TOP_COMMENTS'], - type: options.type || 2 + type: options.type || 2, + commentId: options.comment_id || '' }, target: 'comments-section' } diff --git a/src/proto/youtube.proto b/src/proto/youtube.proto index cfa271814..ffb795a78 100644 --- a/src/proto/youtube.proto +++ b/src/proto/youtube.proto @@ -162,6 +162,7 @@ message GetCommentsSectionParams { required string video_id = 4; required int32 sort_by = 6; required int32 type = 15; + optional string comment_id = 16; } message RepliesOptions { From 6d0bc89be18f27a8ce74517f5cab5020d6790328 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Wed, 24 Jul 2024 14:54:37 -0400 Subject: [PATCH 03/18] fix(parser): ignore MiniGameCardView node (#692) --- src/parser/parser.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 296f9f1ea..ef28de6fb 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -77,7 +77,8 @@ const IGNORED_LIST = new Set([ 'BrandVideoSingleton', 'StatementBanner', 'GuideSigninPromo', - 'AdsEngagementPanelContent' + 'AdsEngagementPanelContent', + 'MiniGameCardView' ]); const RUNTIME_NODES = new Map(Object.entries(YTNodes)); From 090539b28f9bc3387d01e37325ba5741b33b1765 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Wed, 24 Jul 2024 14:55:20 -0400 Subject: [PATCH 04/18] feat(parser): add classdata to unhandled parse errors (#691) --- src/parser/parser.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/parser/parser.ts b/src/parser/parser.ts index ef28de6fb..0915f4e31 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -95,7 +95,8 @@ let ERROR_HANDLER: ParserErrorHandler = ({ classname, ...context }: ParserError) new InnertubeError( `Something went wrong at ${classname}!\n` + `This is a bug, please report it at ${Platform.shim.info.bugs_url}`, { - stack: context.error.stack + stack: context.error.stack, + classdata: JSON.stringify(context.classdata, null, 2) } ) ); From 3048f70f60756884bd7b591d770f7b6343cfa259 Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:07:00 +0200 Subject: [PATCH 05/18] fix(Player): Fix extracting the n-token decipher algorithm again (#701) --- src/core/Player.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/core/Player.ts b/src/core/Player.ts index dd7f5731b..ffbb80d6b 100644 --- a/src/core/Player.ts +++ b/src/core/Player.ts @@ -220,20 +220,13 @@ export default class Player { } static extractNSigSourceCode(data: string): string { - let sc = getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}'); + const match = data.match(/b=(?:a\.split\(|String\.prototype\.split\.call\(a,)""\).*?\}return (?:b\.join\(|Array\.prototype\.join\.call\(b,)""\)\}/s); - if (sc) - return `function descramble_nsig(a) { let b=a.split("")${sc}} return b.join(""); } descramble_nsig(nsig)`; - - sc = getStringBetweenStrings(data, 'b=String.prototype.split.call(a,"")', '}return Array.prototype.join.call(b,"")}'); - - if (sc) - return `function descramble_nsig(a) { let b=String.prototype.split.call(a, "")${sc}} return Array.prototype.join.call(b, ""); } descramble_nsig(nsig)`; - - // We really should throw an error here to avoid errors later, returning a pass-through function for backwards-compatibility - Log.warn(TAG, 'Failed to extract n-token decipher algorithm'); + if (!match) { + throw new PlayerError('Failed to extract n-token decipher algorithm'); + } - return 'function descramble_nsig(a) { return a; } descramble_nsig(nsig)'; + return `function descramble_nsig(a) { let ${match[0]} descramble_nsig(nsig)`; } get url(): string { From 6765f4e0d791c657fc7411e9cdd2c0f9284e9982 Mon Sep 17 00:00:00 2001 From: Luan Date: Thu, 25 Jul 2024 10:48:24 -0300 Subject: [PATCH 06/18] fix(Player): Bump cache version (#702) We should always do this after updating the sig/nsig code, it's so that the old cache gets ignored : ). --- src/core/Player.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Player.ts b/src/core/Player.ts index ffbb80d6b..0ded720c7 100644 --- a/src/core/Player.ts +++ b/src/core/Player.ts @@ -234,6 +234,6 @@ export default class Player { } static get LIBRARY_VERSION(): number { - return 10; + return 11; } } \ No newline at end of file From 1e29019a074976365f9f99d2c296e05dc4f994c3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 25 Jul 2024 10:52:50 -0300 Subject: [PATCH 07/18] chore(main): release 10.2.0 (#688) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 17 +++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecdbdd98e..5f5273e16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [10.2.0](https://github.com/LuanRT/YouTube.js/compare/v10.1.0...v10.2.0) (2024-07-25) + + +### Features + +* **Format:** Add `is_secondary` for detecting secondary audio tracks ([#697](https://github.com/LuanRT/YouTube.js/issues/697)) ([a352dde](https://github.com/LuanRT/YouTube.js/commit/a352ddeb9db001e99f49025048ad0942d84f1b3e)) +* **parser:** add classdata to unhandled parse errors ([#691](https://github.com/LuanRT/YouTube.js/issues/691)) ([090539b](https://github.com/LuanRT/YouTube.js/commit/090539b28f9bc3387d01e37325ba5741b33b1765)) +* **proto:** Add `comment_id` to commentSectionParams ([#693](https://github.com/LuanRT/YouTube.js/issues/693)) ([a5f6209](https://github.com/LuanRT/YouTube.js/commit/a5f62093a18705fc822abd86beaa81788b6535ce)) + + +### Bug Fixes + +* **parser:** ignore MiniGameCardView node ([#692](https://github.com/LuanRT/YouTube.js/issues/692)) ([6d0bc89](https://github.com/LuanRT/YouTube.js/commit/6d0bc89be18f27a8ce74517f5cab5020d6790328)) +* **parser:** ThumbnailView background color ([#686](https://github.com/LuanRT/YouTube.js/issues/686)) ([0f8f92a](https://github.com/LuanRT/YouTube.js/commit/0f8f92a28a5b6143e890626b22ce570730a0cf09)) +* **Player:** Bump cache version ([#702](https://github.com/LuanRT/YouTube.js/issues/702)) ([6765f4e](https://github.com/LuanRT/YouTube.js/commit/6765f4e0d791c657fc7411e9cdd2c0f9284e9982)) +* **Player:** Fix extracting the n-token decipher algorithm again ([#701](https://github.com/LuanRT/YouTube.js/issues/701)) ([3048f70](https://github.com/LuanRT/YouTube.js/commit/3048f70f60756884bd7b591d770f7b6343cfa259)) + ## [10.1.0](https://github.com/LuanRT/YouTube.js/compare/v10.0.0...v10.1.0) (2024-07-10) diff --git a/package-lock.json b/package-lock.json index 22480c6d3..f2a6e8e64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "youtubei.js", - "version": "10.1.0", + "version": "10.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "youtubei.js", - "version": "10.1.0", + "version": "10.2.0", "funding": [ "https://github.com/sponsors/LuanRT" ], diff --git a/package.json b/package.json index b5a8345b9..77ce78f9e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "youtubei.js", - "version": "10.1.0", + "version": "10.2.0", "description": "A wrapper around YouTube's private API. Supports YouTube, YouTube Music, YouTube Kids and YouTube Studio (WIP).", "type": "module", "types": "./dist/src/platform/lib.d.ts", From a9bf225a62108e47a50316235a83a814c682d745 Mon Sep 17 00:00:00 2001 From: Dave Nicolson Date: Fri, 26 Jul 2024 03:12:57 +0200 Subject: [PATCH 08/18] feat(parser): Add `EomSettingsDisclaimer` node (#703) --- src/parser/classes/EomSettingsDisclaimer.ts | 22 +++++++++++++++++++++ src/parser/nodes.ts | 1 + 2 files changed, 23 insertions(+) create mode 100644 src/parser/classes/EomSettingsDisclaimer.ts diff --git a/src/parser/classes/EomSettingsDisclaimer.ts b/src/parser/classes/EomSettingsDisclaimer.ts new file mode 100644 index 000000000..51031347f --- /dev/null +++ b/src/parser/classes/EomSettingsDisclaimer.ts @@ -0,0 +1,22 @@ +import Text from './misc/Text.js'; +import { YTNode } from '../helpers.js'; +import { type RawNode } from '../index.js'; + +export default class EomSettingsDisclaimer extends YTNode { + static type = 'EomSettingsDisclaimer'; + + disclaimer: Text; + info_icon: { + icon_type: string + }; + usage_scenario: string; + + constructor(data: RawNode) { + super(); + this.disclaimer = new Text(data.disclaimer); + this.info_icon = { + icon_type: data.infoIcon.iconType + }; + this.usage_scenario = data.usageScenario; + } +} diff --git a/src/parser/nodes.ts b/src/parser/nodes.ts index 46d2ad1b3..365b8eea8 100644 --- a/src/parser/nodes.ts +++ b/src/parser/nodes.ts @@ -120,6 +120,7 @@ export { default as EndScreenPlaylist } from './classes/EndScreenPlaylist.js'; export { default as EndScreenVideo } from './classes/EndScreenVideo.js'; export { default as EngagementPanelSectionList } from './classes/EngagementPanelSectionList.js'; export { default as EngagementPanelTitleHeader } from './classes/EngagementPanelTitleHeader.js'; +export { default as EomSettingsDisclaimer } from './classes/EomSettingsDisclaimer.js'; export { default as ExpandableMetadata } from './classes/ExpandableMetadata.js'; export { default as ExpandableTab } from './classes/ExpandableTab.js'; export { default as ExpandableVideoDescriptionBody } from './classes/ExpandableVideoDescriptionBody.js'; From 3153375bcaa6c03afba9da8474e6a9d37471ed29 Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Fri, 26 Jul 2024 16:15:12 +0200 Subject: [PATCH 09/18] fix(HTTPClient): Adjust more context fields for the iOS client (#705) --- src/utils/HTTPClient.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utils/HTTPClient.ts b/src/utils/HTTPClient.ts index a79625322..0e6ea657c 100644 --- a/src/utils/HTTPClient.ts +++ b/src/utils/HTTPClient.ts @@ -181,10 +181,14 @@ export default class HTTPClient { switch (client) { case 'iOS': + ctx.client.deviceMake = 'Apple'; ctx.client.deviceModel = Constants.CLIENTS.iOS.DEVICE_MODEL; ctx.client.clientVersion = Constants.CLIENTS.iOS.VERSION; ctx.client.clientName = Constants.CLIENTS.iOS.NAME; ctx.client.platform = 'MOBILE'; + ctx.client.osName = 'iOS'; + delete ctx.client.browserName; + delete ctx.client.browserVersion; break; case 'YTMUSIC': ctx.client.clientVersion = Constants.CLIENTS.YTMUSIC.VERSION; From 9b9fb8213134bdeba32e3d8aad13fbc70d52cf01 Mon Sep 17 00:00:00 2001 From: Luan Date: Tue, 30 Jul 2024 18:49:24 -0300 Subject: [PATCH 10/18] refactor: Clean up & fix old code Other changes: - Renamed "getShortsWatchItem" to "getShortsVideoInfo". - Fixed `ShortFormVideoInfo`. This never worked for me ever since it was introduced. Turned out it was just implemented incorrectly. - Moved `basic_info` extraction to `MediaInfo`. Less of a pain to maintain as we only have to modify one file. - Removed unneeded tsdoc comments. - Fixed `Innertube#getStreamingData()`. Now it actually returns a deciphered format. - Simplified some types. --- src/Innertube.ts | 112 ++++----------- .../endpoints/reel/ReelItemWatchEndpoint.ts | 20 +++ ...dpoint.ts => ReelWatchSequenceEndpoint.ts} | 6 +- src/core/endpoints/reel/WatchEndpoint.ts | 18 --- src/core/endpoints/reel/index.ts | 4 +- src/core/mixins/MediaInfo.ts | 62 ++++++++- src/parser/classes/Command.ts | 14 -- src/parser/classes/CompactVideo.ts | 4 +- src/parser/classes/NavigationEndpoint.ts | 7 +- src/parser/classes/ReelPlayerOverlay.ts | 3 +- .../classes/ThumbnailOverlayToggleButton.ts | 11 +- .../livechat/UpdateToggleButtonTextAction.ts | 7 +- src/parser/classes/menus/MultiPageMenu.ts | 11 +- src/parser/classes/menus/SimpleMenuHeader.ts | 8 +- src/parser/nodes.ts | 1 - src/parser/parser.ts | 31 +++-- src/parser/types/ParsedResponse.ts | 131 +++++------------- src/parser/types/RawResponse.ts | 26 +++- src/parser/youtube/AccountInfo.ts | 6 +- src/parser/youtube/Analytics.ts | 6 +- src/parser/youtube/Channel.ts | 4 +- src/parser/youtube/Comments.ts | 6 +- src/parser/youtube/Guide.ts | 6 +- src/parser/youtube/History.ts | 6 +- src/parser/youtube/HomeFeed.ts | 10 +- src/parser/youtube/ItemMenu.ts | 6 +- src/parser/youtube/Library.ts | 8 +- src/parser/youtube/LiveChat.ts | 6 +- src/parser/youtube/NotificationsMenu.ts | 11 +- src/parser/youtube/Search.ts | 19 +-- src/parser/youtube/Settings.ts | 6 +- src/parser/youtube/SmoothedQueue.ts | 6 +- src/parser/youtube/TimeWatched.ts | 6 +- src/parser/youtube/TranscriptInfo.ts | 4 + src/parser/youtube/VideoInfo.ts | 56 +------- src/parser/ytkids/Channel.ts | 6 +- src/parser/ytkids/HomeFeed.ts | 6 +- src/parser/ytkids/Search.ts | 8 +- src/parser/ytkids/VideoInfo.ts | 22 +-- src/parser/ytmusic/Album.ts | 6 +- src/parser/ytmusic/Artist.ts | 6 +- src/parser/ytmusic/Explore.ts | 6 +- src/parser/ytmusic/HomeFeed.ts | 6 +- src/parser/ytmusic/Library.ts | 9 +- src/parser/ytmusic/Playlist.ts | 6 +- src/parser/ytmusic/Recap.ts | 6 +- src/parser/ytmusic/TrackInfo.ts | 27 +--- src/parser/ytshorts/ShortFormVideoInfo.ts | 55 ++++++++ src/parser/ytshorts/VideoInfo.ts | 56 -------- src/parser/ytshorts/index.ts | 2 +- .../messages/youtube/InnertubePayload.ts | 17 --- src/types/Endpoints.ts | 25 ++-- 52 files changed, 361 insertions(+), 556 deletions(-) create mode 100644 src/core/endpoints/reel/ReelItemWatchEndpoint.ts rename src/core/endpoints/reel/{WatchSequenceEndpoint.ts => ReelWatchSequenceEndpoint.ts} (59%) delete mode 100644 src/core/endpoints/reel/WatchEndpoint.ts delete mode 100644 src/parser/classes/Command.ts create mode 100644 src/parser/ytshorts/ShortFormVideoInfo.ts delete mode 100644 src/parser/ytshorts/VideoInfo.ts diff --git a/src/Innertube.ts b/src/Innertube.ts index cab92741d..40ee2ca68 100644 --- a/src/Innertube.ts +++ b/src/Innertube.ts @@ -29,7 +29,7 @@ import { VideoInfo } from './parser/youtube/index.js'; -import { VideoInfo as ShortsVideoInfo } from './parser/ytshorts/index.js'; +import { ShortFormVideoInfo } from './parser/ytshorts/index.js'; import NavigationEndpoint from './parser/classes/NavigationEndpoint.js'; @@ -37,7 +37,6 @@ import * as Proto from './proto/index.js'; import * as Constants from './utils/Constants.js'; import { InnertubeError, generateRandomString, throwIfMissing } from './utils/Utils.js'; - import type { ApiResponse } from './core/Actions.js'; import type { INextRequest } from './types/index.js'; import type { IBrowseResponse, IParsedResponse } from './parser/types/index.js'; @@ -71,13 +70,8 @@ export default class Innertube { return new Innertube(await Session.create(config)); } - /** - * Retrieves video info. - * @param target - The video id or `NavigationEndpoint`. - * @param client - The client to use. - */ async getInfo(target: string | NavigationEndpoint, client?: InnerTubeClient): Promise { - throwIfMissing({ target }); + throwIfMissing({ target: target }); let next_payload: INextRequest; @@ -93,7 +87,7 @@ export default class Innertube { video_id: target }); } else { - throw new InnertubeError('Invalid target, expected either a video id or a valid NavigationEndpoint', target); + throw new InnertubeError('Invalid target. Expected a video id or NavigationEndpoint.', target); } if (!next_payload.videoId) @@ -115,11 +109,6 @@ export default class Innertube { return new VideoInfo(response, this.actions, cpn); } - /** - * Retrieves basic video info. - * @param video_id - The video id. - * @param client - The client to use. - */ async getBasicInfo(video_id: string, client?: InnerTubeClient): Promise { throwIfMissing({ video_id }); @@ -136,37 +125,26 @@ export default class Innertube { return new VideoInfo([ response ], this.actions, cpn); } - /** - * Retrieves shorts info. - * @param short_id - The short id. - * @param client - The client to use. - */ - async getShortsWatchItem(short_id: string, client?: InnerTubeClient): Promise { - throwIfMissing({ short_id }); + async getShortsVideoInfo(video_id: string, client?: InnerTubeClient): Promise { + throwIfMissing({ video_id }); - const watchResponse = this.actions.execute( - Reel.WatchEndpoint.PATH, Reel.WatchEndpoint.build({ - short_id: short_id, - client: client - }) + const watch_response = this.actions.execute( + Reel.ReelItemWatchEndpoint.PATH, Reel.ReelItemWatchEndpoint.build({ video_id, client }) ); - const sequenceResponse = this.actions.execute( - Reel.WatchSequenceEndpoint.PATH, Reel.WatchSequenceEndpoint.build({ - sequenceParams: Proto.encodeReelSequence(short_id) + const sequence_response = this.actions.execute( + Reel.ReelWatchSequenceEndpoint.PATH, Reel.ReelWatchSequenceEndpoint.build({ + sequence_params: Proto.encodeReelSequence(video_id) }) ); - const response = await Promise.all([ watchResponse, sequenceResponse ]); + const response = await Promise.all([ watch_response, sequence_response ]); + + const cpn = generateRandomString(16); - return new ShortsVideoInfo(response, this.actions); + return new ShortFormVideoInfo([ response[0] ], this.actions, cpn, response[1]); } - /** - * Searches a given query. - * @param query - The search query. - * @param filters - Search filters. - */ async search(query: string, filters: SearchFilters = {}): Promise { throwIfMissing({ query }); @@ -179,10 +157,6 @@ export default class Innertube { return new Search(this.actions, response); } - /** - * Retrieves search suggestions for a given query. - * @param query - The search query. - */ async getSearchSuggestions(query: string): Promise { throwIfMissing({ query }); @@ -204,11 +178,6 @@ export default class Innertube { return suggestions; } - /** - * Retrieves comments for a video. - * @param video_id - The video id. - * @param sort_by - Sorting options. - */ async getComments(video_id: string, sort_by?: 'TOP_COMMENTS' | 'NEWEST_FIRST'): Promise { throwIfMissing({ video_id }); @@ -223,9 +192,6 @@ export default class Innertube { return new Comments(this.actions, response.data); } - /** - * Retrieves YouTube's home feed (aka recommendations). - */ async getHomeFeed(): Promise { const response = await this.actions.execute( BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: 'FEwhat_to_watch' }) @@ -241,9 +207,6 @@ export default class Innertube { return new Guide(response.data); } - /** - * Returns the account's library. - */ async getLibrary(): Promise { const response = await this.actions.execute( BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: 'FElibrary' }) @@ -251,10 +214,6 @@ export default class Innertube { return new Library(this.actions, response); } - /** - * Retrieves watch history. - * Which can also be achieved with {@link getLibrary}. - */ async getHistory(): Promise { const response = await this.actions.execute( BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: 'FEhistory' }) @@ -262,9 +221,6 @@ export default class Innertube { return new History(this.actions, response); } - /** - * Retrieves Trending content. - */ async getTrending(): Promise> { const response = await this.actions.execute( BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: 'FEtrending' }), parse: true } @@ -272,9 +228,6 @@ export default class Innertube { return new TabbedFeed(this.actions, response); } - /** - * Retrieves Subscriptions feed. - */ async getSubscriptionsFeed(): Promise> { const response = await this.actions.execute( BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: 'FEsubscriptions' }), parse: true } @@ -282,9 +235,6 @@ export default class Innertube { return new Feed(this.actions, response); } - /** - * Retrieves Channels feed. - */ async getChannelsFeed(): Promise> { const response = await this.actions.execute( BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: 'FEchannels' }), parse: true } @@ -292,10 +242,6 @@ export default class Innertube { return new Feed(this.actions, response); } - /** - * Retrieves contents for a given channel. - * @param id - Channel id - */ async getChannel(id: string): Promise { throwIfMissing({ id }); const response = await this.actions.execute( @@ -304,9 +250,6 @@ export default class Innertube { return new Channel(this.actions, response); } - /** - * Retrieves notifications. - */ async getNotifications(): Promise { const response = await this.actions.execute( GetNotificationMenuEndpoint.PATH, GetNotificationMenuEndpoint.build({ @@ -316,17 +259,14 @@ export default class Innertube { return new NotificationsMenu(this.actions, response); } - /** - * Retrieves unseen notifications count. - */ async getUnseenNotificationsCount(): Promise { const response = await this.actions.execute(Notification.GetUnseenCountEndpoint.PATH); - // TODO: properly parse this + // FIXME: properly parse this. return response.data?.unseenCount || response.data?.actions?.[0].updateNotificationsUnseenCountAction?.unseenCount || 0; } /** - * Retrieves playlists. + * Retrieves the user's playlists. */ async getPlaylists(): Promise> { const response = await this.actions.execute( @@ -335,10 +275,6 @@ export default class Innertube { return new Feed(this.actions, response); } - /** - * Retrieves playlist contents. - * @param id - Playlist id - */ async getPlaylist(id: string): Promise { throwIfMissing({ id }); @@ -353,10 +289,6 @@ export default class Innertube { return new Playlist(this.actions, response); } - /** - * Retrieves a given hashtag's page. - * @param hashtag - The hashtag to fetch. - */ async getHashtag(hashtag: string): Promise { throwIfMissing({ hashtag }); @@ -380,11 +312,15 @@ export default class Innertube { */ async getStreamingData(video_id: string, options: FormatOptions = {}): Promise { const info = await this.getBasicInfo(video_id); - return info.chooseFormat(options); + + const format = info.chooseFormat(options); + format.url = format.decipher(this.#session.player); + + return format; } /** - * Downloads a given video. If you only need the direct download link see {@link getStreamingData}. + * Downloads a given video. If all you need the direct download link, see {@link getStreamingData}. * If you wish to retrieve the video info too, have a look at {@link getBasicInfo} or {@link getInfo}. * @param video_id - The video id. * @param options - Download options. @@ -402,6 +338,10 @@ export default class Innertube { const response = await this.actions.execute( ResolveURLEndpoint.PATH, { ...ResolveURLEndpoint.build({ url }), parse: true } ); + + if (!response.endpoint) + throw new InnertubeError('Failed to resolve URL. Expected a NavigationEndpoint but got undefined', response); + return response.endpoint; } diff --git a/src/core/endpoints/reel/ReelItemWatchEndpoint.ts b/src/core/endpoints/reel/ReelItemWatchEndpoint.ts new file mode 100644 index 000000000..fdc2d8de9 --- /dev/null +++ b/src/core/endpoints/reel/ReelItemWatchEndpoint.ts @@ -0,0 +1,20 @@ +import type { IReelItemWatchRequest, ReelItemWatchEndpointOptions } from '../../../types/index.js'; + +export const PATH = '/reel/reel_item_watch'; + +/** + * Builds a `/reel/reel_watch_sequence` request payload. + * @param opts - The options to use. + * @returns The payload. + */ +export function build(opts: ReelItemWatchEndpointOptions): IReelItemWatchRequest { + return { + disablePlayerResponse: false, + playerRequest: { + videoId: opts.video_id, + params: opts.params ?? 'CAUwAg%3D%3D' + }, + params: opts.params ?? 'CAUwAg%3D%3D', + client: opts.client + }; +} \ No newline at end of file diff --git a/src/core/endpoints/reel/WatchSequenceEndpoint.ts b/src/core/endpoints/reel/ReelWatchSequenceEndpoint.ts similarity index 59% rename from src/core/endpoints/reel/WatchSequenceEndpoint.ts rename to src/core/endpoints/reel/ReelWatchSequenceEndpoint.ts index 7deb8982e..c0313e3b5 100644 --- a/src/core/endpoints/reel/WatchSequenceEndpoint.ts +++ b/src/core/endpoints/reel/ReelWatchSequenceEndpoint.ts @@ -1,4 +1,4 @@ -import type { IReelSequenceRequest, ReelWatchSequenceEndpointOptions } from '../../../types/index.js'; +import type { IReelWatchSequenceRequest, ReelWatchSequenceEndpointOptions } from '../../../types/index.js'; export const PATH = '/reel/reel_watch_sequence'; @@ -7,8 +7,8 @@ export const PATH = '/reel/reel_watch_sequence'; * @param opts - The options to use. * @returns The payload. */ -export function build(opts: ReelWatchSequenceEndpointOptions): IReelSequenceRequest { +export function build(opts: ReelWatchSequenceEndpointOptions): IReelWatchSequenceRequest { return { - sequenceParams: opts.sequenceParams + sequenceParams: opts.sequence_params }; } \ No newline at end of file diff --git a/src/core/endpoints/reel/WatchEndpoint.ts b/src/core/endpoints/reel/WatchEndpoint.ts deleted file mode 100644 index 647ec422c..000000000 --- a/src/core/endpoints/reel/WatchEndpoint.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { IReelWatchRequest, ReelWatchEndpointOptions } from '../../../types/index.js'; - -export const PATH = '/reel/reel_item_watch'; - -/** - * Builds a `/reel/reel_watch_sequence` request payload. - * @param opts - The options to use. - * @returns The payload. - */ -export function build(opts: ReelWatchEndpointOptions): IReelWatchRequest { - return { - playerRequest: { - videoId: opts.short_id, - params: opts.params ?? 'CAUwAg%3D%3D' - }, - params: opts.params ?? 'CAUwAg%3D%3D' - }; -} \ No newline at end of file diff --git a/src/core/endpoints/reel/index.ts b/src/core/endpoints/reel/index.ts index 4953dbbb2..4fd216d50 100644 --- a/src/core/endpoints/reel/index.ts +++ b/src/core/endpoints/reel/index.ts @@ -1,2 +1,2 @@ -export * as WatchEndpoint from './WatchEndpoint.js'; -export * as WatchSequenceEndpoint from './WatchSequenceEndpoint.js'; \ No newline at end of file +export * as ReelItemWatchEndpoint from './ReelItemWatchEndpoint.js'; +export * as ReelWatchSequenceEndpoint from './ReelWatchSequenceEndpoint.js'; \ No newline at end of file diff --git a/src/core/mixins/MediaInfo.ts b/src/core/mixins/MediaInfo.ts index 55abb0a7c..7f3259312 100644 --- a/src/core/mixins/MediaInfo.ts +++ b/src/core/mixins/MediaInfo.ts @@ -5,27 +5,43 @@ import { getStreamingInfo } from '../../utils/StreamingInfo.js'; import { Parser } from '../../parser/index.js'; import { TranscriptInfo } from '../../parser/youtube/index.js'; import ContinuationItem from '../../parser/classes/ContinuationItem.js'; +import PlayerMicroformat from '../../parser/classes/PlayerMicroformat.js'; +import MicroformatData from '../../parser/classes/MicroformatData.js'; import type { ApiResponse, Actions } from '../index.js'; -import type { INextResponse, IPlayerConfig, IPlayerResponse } from '../../parser/index.js'; +import type { INextResponse, IPlayabilityStatus, IPlaybackTracking, IPlayerConfig, IPlayerResponse, IStreamingData } from '../../parser/index.js'; import type { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../../types/FormatUtils.js'; import type Format from '../../parser/classes/misc/Format.js'; import type { DashOptions } from '../../types/DashOptions.js'; +import type { ObservedArray } from '../../parser/helpers.js'; + +import type CardCollection from '../../parser/classes/CardCollection.js'; +import type Endscreen from '../../parser/classes/Endscreen.js'; +import type PlayerAnnotationsExpanded from '../../parser/classes/PlayerAnnotationsExpanded.js'; +import type PlayerCaptionsTracklist from '../../parser/classes/PlayerCaptionsTracklist.js'; +import type PlayerLiveStoryboardSpec from '../../parser/classes/PlayerLiveStoryboardSpec.js'; +import type PlayerStoryboardSpec from '../../parser/classes/PlayerStoryboardSpec.js'; export default class MediaInfo { #page: [IPlayerResponse, INextResponse?]; #actions: Actions; #cpn: string; - #playback_tracking; - streaming_data; - playability_status; - player_config: IPlayerConfig; + #playback_tracking?: IPlaybackTracking; + basic_info; + annotations?: ObservedArray; + storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec; + endscreen?: Endscreen; + captions?: PlayerCaptionsTracklist; + cards?: CardCollection; + streaming_data?: IStreamingData; + playability_status?: IPlayabilityStatus; + player_config?: IPlayerConfig; constructor(data: [ApiResponse, ApiResponse?], actions: Actions, cpn: string) { this.#actions = actions; - const info = Parser.parseResponse(data[0].data); - const next = data?.[1]?.data ? Parser.parseResponse(data[1].data) : undefined; + const info = Parser.parseResponse(data[0].data.playerResponse ? data[0].data.playerResponse : data[0].data); + const next = data[1]?.data ? Parser.parseResponse(data[1].data) : undefined; this.#page = [ info, next ]; this.#cpn = cpn; @@ -33,6 +49,38 @@ export default class MediaInfo { if (info.playability_status?.status === 'ERROR') throw new InnertubeError('This video is unavailable', info.playability_status); + if (info.microformat && !info.microformat?.is(PlayerMicroformat, MicroformatData)) + throw new InnertubeError('Unsupported microformat', info.microformat); + + this.basic_info = { // This type is inferred so no need for an explicit type + ...info.video_details, + /** + * Microformat is a bit redundant, so only + * a few things there are interesting to us. + */ + ...{ + embed: info.microformat?.is(PlayerMicroformat) ? info.microformat?.embed : null, + channel: info.microformat?.is(PlayerMicroformat) ? info.microformat?.channel : null, + is_unlisted: info.microformat?.is_unlisted, + is_family_safe: info.microformat?.is_family_safe, + category: info.microformat?.is(PlayerMicroformat) ? info.microformat?.category : null, + has_ypc_metadata: info.microformat?.is(PlayerMicroformat) ? info.microformat?.has_ypc_metadata : null, + start_timestamp: info.microformat?.is(PlayerMicroformat) ? info.microformat.start_timestamp : null, + end_timestamp: info.microformat?.is(PlayerMicroformat) ? info.microformat.end_timestamp : null, + view_count: info.microformat?.is(PlayerMicroformat) && isNaN(info.video_details?.view_count as number) ? info.microformat.view_count : info.video_details?.view_count, + url_canonical: info.microformat?.is(MicroformatData) ? info.microformat?.url_canonical : null, + tags: info.microformat?.is(MicroformatData) ? info.microformat?.tags : null + }, + like_count: undefined as number | undefined, + is_liked: undefined as boolean | undefined, + is_disliked: undefined as boolean | undefined + }; + + this.annotations = info.annotations; + this.storyboards = info.storyboards; + this.endscreen = info.endscreen; + this.captions = info.captions; + this.cards = info.cards; this.streaming_data = info.streaming_data; this.playability_status = info.playability_status; this.player_config = info.player_config; diff --git a/src/parser/classes/Command.ts b/src/parser/classes/Command.ts deleted file mode 100644 index e9359e7b7..000000000 --- a/src/parser/classes/Command.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { YTNode } from '../helpers.js'; -import type { RawNode } from '../index.js'; -import NavigationEndpoint from './NavigationEndpoint.js'; - -export default class Command extends YTNode { - static type = 'Command'; - - endpoint: NavigationEndpoint; - - constructor(data: RawNode) { - super(); - this.endpoint = new NavigationEndpoint(data); - } -} \ No newline at end of file diff --git a/src/parser/classes/CompactVideo.ts b/src/parser/classes/CompactVideo.ts index 22ad2c8f7..b43baf9a4 100644 --- a/src/parser/classes/CompactVideo.ts +++ b/src/parser/classes/CompactVideo.ts @@ -13,7 +13,7 @@ export default class CompactVideo extends YTNode { id: string; thumbnails: Thumbnail[]; - rich_thumbnail?: SuperParsedResult; + rich_thumbnail?: YTNode; title: Text; author: Author; view_count: Text; @@ -36,7 +36,7 @@ export default class CompactVideo extends YTNode { this.thumbnails = Thumbnail.fromResponse(data.thumbnail) || null; if (Reflect.has(data, 'richThumbnail')) { - this.rich_thumbnail = Parser.parse(data.richThumbnail); + this.rich_thumbnail = Parser.parseItem(data.richThumbnail); } this.title = new Text(data.title); diff --git a/src/parser/classes/NavigationEndpoint.ts b/src/parser/classes/NavigationEndpoint.ts index 1dfc325b9..db1117913 100644 --- a/src/parser/classes/NavigationEndpoint.ts +++ b/src/parser/classes/NavigationEndpoint.ts @@ -27,8 +27,8 @@ export default class NavigationEndpoint extends YTNode { constructor(data: RawNode) { super(); - if (Reflect.has(data || {}, 'innertubeCommand')) - data = data.innertubeCommand; + if (data && (data.innertubeCommand || data.command)) + data = data.innertubeCommand || data.command; if (Reflect.has(data || {}, 'openPopupAction')) this.open_popup = new OpenPopupAction(data.openPopupAction); @@ -92,13 +92,14 @@ export default class NavigationEndpoint extends YTNode { case 'browseEndpoint': return '/browse'; case 'watchEndpoint': + case 'reelWatchEndpoint': return '/player'; case 'searchEndpoint': return '/search'; case 'watchPlaylistEndpoint': return '/next'; case 'liveChatItemContextMenuEndpoint': - return 'live_chat/get_item_context_menu'; + return '/live_chat/get_item_context_menu'; } } diff --git a/src/parser/classes/ReelPlayerOverlay.ts b/src/parser/classes/ReelPlayerOverlay.ts index cda01cfbf..d1bcb43da 100644 --- a/src/parser/classes/ReelPlayerOverlay.ts +++ b/src/parser/classes/ReelPlayerOverlay.ts @@ -6,6 +6,7 @@ import InfoPanelContainer from './InfoPanelContainer.js'; import LikeButton from './LikeButton.js'; import ReelPlayerHeader from './ReelPlayerHeader.js'; import PivotButton from './PivotButton.js'; +import SubscribeButton from './SubscribeButton.js'; export default class ReelPlayerOverlay extends YTNode { static type = 'ReelPlayerOverlay'; @@ -29,7 +30,7 @@ export default class ReelPlayerOverlay extends YTNode { this.menu = Parser.parseItem(data.menu, Menu); this.next_item_button = Parser.parseItem(data.nextItemButton, Button); this.prev_item_button = Parser.parseItem(data.prevItemButton, Button); - this.subscribe_button_renderer = Parser.parseItem(data.subscribeButtonRenderer, Button); + this.subscribe_button_renderer = Parser.parseItem(data.subscribeButtonRenderer, [ Button, SubscribeButton ]); this.style = data.style; this.view_comments_button = Parser.parseItem(data.viewCommentsButton, Button); this.share_button = Parser.parseItem(data.shareButton, Button); diff --git a/src/parser/classes/ThumbnailOverlayToggleButton.ts b/src/parser/classes/ThumbnailOverlayToggleButton.ts index 899fe3701..a066030d5 100644 --- a/src/parser/classes/ThumbnailOverlayToggleButton.ts +++ b/src/parser/classes/ThumbnailOverlayToggleButton.ts @@ -17,8 +17,8 @@ export default class ThumbnailOverlayToggleButton extends YTNode { untoggled: string; }; - toggled_endpoint: NavigationEndpoint; - untoggled_endpoint: NavigationEndpoint; + toggled_endpoint?: NavigationEndpoint; + untoggled_endpoint?: NavigationEndpoint; constructor(data: RawNode) { super(); @@ -36,7 +36,10 @@ export default class ThumbnailOverlayToggleButton extends YTNode { untoggled: data.untoggledTooltip }; - this.toggled_endpoint = new NavigationEndpoint(data.toggledServiceEndpoint); - this.untoggled_endpoint = new NavigationEndpoint(data.untoggledServiceEndpoint); + if (data.toggledServiceEndpoint) + this.toggled_endpoint = new NavigationEndpoint(data.toggledServiceEndpoint); + + if (data.untoggledServiceEndpoint) + this.untoggled_endpoint = new NavigationEndpoint(data.untoggledServiceEndpoint); } } \ No newline at end of file diff --git a/src/parser/classes/livechat/UpdateToggleButtonTextAction.ts b/src/parser/classes/livechat/UpdateToggleButtonTextAction.ts index fd2d99250..2f5ae0890 100644 --- a/src/parser/classes/livechat/UpdateToggleButtonTextAction.ts +++ b/src/parser/classes/livechat/UpdateToggleButtonTextAction.ts @@ -1,7 +1,8 @@ import Text from '../misc/Text.js'; import { YTNode } from '../../helpers.js'; import type { RawNode } from '../../index.js'; -class UpdateToggleButtonTextAction extends YTNode { + +export default class UpdateToggleButtonTextAction extends YTNode { static type = 'UpdateToggleButtonTextAction'; default_text: string; @@ -14,6 +15,4 @@ class UpdateToggleButtonTextAction extends YTNode { this.toggled_text = new Text(data.toggledText).toString(); this.button_id = data.buttonId; } -} - -export default UpdateToggleButtonTextAction; \ No newline at end of file +} \ No newline at end of file diff --git a/src/parser/classes/menus/MultiPageMenu.ts b/src/parser/classes/menus/MultiPageMenu.ts index f8a191070..ed3944746 100644 --- a/src/parser/classes/menus/MultiPageMenu.ts +++ b/src/parser/classes/menus/MultiPageMenu.ts @@ -1,18 +1,19 @@ -import { YTNode, type SuperParsedResult } from '../../helpers.js'; +import type { ObservedArray } from '../../helpers.js'; +import { YTNode } from '../../helpers.js'; import type { RawNode } from '../../index.js'; import { Parser } from '../../index.js'; export default class MultiPageMenu extends YTNode { static type = 'MultiPageMenu'; - header: SuperParsedResult; - sections: SuperParsedResult; + header: YTNode; + sections: ObservedArray; style: string; constructor(data: RawNode) { super(); - this.header = Parser.parse(data.header); - this.sections = Parser.parse(data.sections); + this.header = Parser.parseItem(data.header); + this.sections = Parser.parseArray(data.sections); this.style = data.style; } } \ No newline at end of file diff --git a/src/parser/classes/menus/SimpleMenuHeader.ts b/src/parser/classes/menus/SimpleMenuHeader.ts index e0912e65a..1f2c6682e 100644 --- a/src/parser/classes/menus/SimpleMenuHeader.ts +++ b/src/parser/classes/menus/SimpleMenuHeader.ts @@ -1,19 +1,19 @@ -import type { SuperParsedResult } from '../../helpers.js'; +import type { ObservedArray } from '../../helpers.js'; import { YTNode } from '../../helpers.js'; import type { RawNode } from '../../index.js'; import { Parser } from '../../index.js'; +import Button from '../Button.js'; import Text from '../misc/Text.js'; export default class SimpleMenuHeader extends YTNode { static type = 'SimpleMenuHeader'; title: Text; - buttons: SuperParsedResult; + buttons: ObservedArray