diff --git a/CHANGELOG.md b/CHANGELOG.md index ecdbdd98e..d926fe080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## [10.3.0](https://github.com/LuanRT/YouTube.js/compare/v10.2.0...v10.3.0) (2024-08-01) + + +### Features + +* **parser:** Add `EomSettingsDisclaimer` node ([#703](https://github.com/LuanRT/YouTube.js/issues/703)) ([a9bf225](https://github.com/LuanRT/YouTube.js/commit/a9bf225a62108e47a50316235a83a814c682d745)) +* **PlaylistManager:** Add ability to remove videos by set ID ([#715](https://github.com/LuanRT/YouTube.js/issues/715)) ([d85fbc5](https://github.com/LuanRT/YouTube.js/commit/d85fbc56cf0fd7367b182ae36e65c1701bc5e62d)) + + +### Bug Fixes + +* **HTTPClient:** Adjust more context fields for the iOS client ([#705](https://github.com/LuanRT/YouTube.js/issues/705)) ([3153375](https://github.com/LuanRT/YouTube.js/commit/3153375bcaa6c03afba9da8474e6a9d37471ed29)) + +## [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..f9f151ffb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "youtubei.js", - "version": "10.1.0", + "version": "10.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "youtubei.js", - "version": "10.1.0", + "version": "10.3.0", "funding": [ "https://github.com/sponsors/LuanRT" ], "license": "MIT", "dependencies": { - "jintr": "^2.0.0", + "jintr": "^2.1.1", "tslib": "^2.5.0", "undici": "^5.19.1" }, @@ -2178,9 +2178,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "bin": { "acorn": "bin/acorn" }, @@ -5829,9 +5829,9 @@ } }, "node_modules/jintr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jintr/-/jintr-2.0.0.tgz", - "integrity": "sha512-RiVlevxttZ4eHEYB2dXKXDXluzHfRuw0DJQGsYuKCc5IvZj5/GbOakeqVX+Bar/G9kTty9xDJREcxukurkmYLA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/jintr/-/jintr-2.1.1.tgz", + "integrity": "sha512-89cwX4ouogeDGOBsEVsVYsnWWvWjchmwXBB4kiBhmjOKw19FiOKhNhMhpxhTlK2ctl7DS+d/ethfmuBpzoNNgA==", "funding": [ "https://github.com/sponsors/LuanRT" ], diff --git a/package.json b/package.json index b5a8345b9..95b4f32b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "youtubei.js", - "version": "10.1.0", + "version": "10.3.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", @@ -103,7 +103,7 @@ }, "license": "MIT", "dependencies": { - "jintr": "^2.0.0", + "jintr": "^2.1.1", "tslib": "^2.5.0", "undici": "^5.19.1" }, 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/Player.ts b/src/core/Player.ts index dd7f5731b..dc6f4cf64 100644 --- a/src/core/Player.ts +++ b/src/core/Player.ts @@ -1,5 +1,5 @@ import { Log, LZW, Constants } from '../utils/index.js'; -import { Platform, getRandomUserAgent, getStringBetweenStrings, PlayerError } from '../utils/Utils.js'; +import { Platform, getRandomUserAgent, getStringBetweenStrings, findFunction, PlayerError } from '../utils/Utils.js'; import type { ICache, FetchFunction } from '../types/index.js'; const TAG = 'Player'; @@ -8,16 +8,16 @@ const TAG = 'Player'; * Represents YouTube's player script. This is required to decipher signatures. */ export default class Player { - nsig_sc; - sig_sc; - sts; - player_id; + player_id: string; + sts: number; + nsig_sc?: string; + sig_sc?: string; - constructor(signature_timestamp: number, sig_sc: string, nsig_sc: string, player_id: string) { + constructor(player_id: string, signature_timestamp: number, sig_sc?: string, nsig_sc?: string) { + this.player_id = player_id; + this.sts = signature_timestamp; this.nsig_sc = nsig_sc; this.sig_sc = sig_sc; - this.sts = signature_timestamp; - this.player_id = player_id; } static async create(cache: ICache | undefined, fetch: FetchFunction = Platform.shim.fetch): Promise { @@ -67,7 +67,7 @@ export default class Player { Log.info(TAG, `Got signature timestamp (${sig_timestamp}) and algorithms needed to decipher signatures.`); - return await Player.fromSource(cache, sig_timestamp, sig_sc, nsig_sc, player_id); + return await Player.fromSource(player_id, sig_timestamp, cache, sig_sc, nsig_sc); } decipher(url?: string, signature_cipher?: string, cipher?: string, this_response_nsig_cache?: Map): string { @@ -79,7 +79,7 @@ export default class Player { const args = new URLSearchParams(url); const url_components = new URL(args.get('url') || url); - if (signature_cipher || cipher) { + if (this.sig_sc && (signature_cipher || cipher)) { const signature = Platform.shim.eval(this.sig_sc, { sig: args.get('s') }); @@ -98,7 +98,7 @@ export default class Player { const n = url_components.searchParams.get('n'); - if (n) { + if (this.nsig_sc && n) { let nsig; if (this_response_nsig_cache && this_response_nsig_cache.has(n)) { @@ -174,17 +174,18 @@ export default class Player { const sig_sc = LZW.decompress(new TextDecoder().decode(sig_buf)); const nsig_sc = LZW.decompress(new TextDecoder().decode(nsig_buf)); - return new Player(sig_timestamp, sig_sc, nsig_sc, player_id); + return new Player(player_id, sig_timestamp, sig_sc, nsig_sc); } - static async fromSource(cache: ICache | undefined, sig_timestamp: number, sig_sc: string, nsig_sc: string, player_id: string): Promise { - const player = new Player(sig_timestamp, sig_sc, nsig_sc, player_id); + static async fromSource(player_id: string, sig_timestamp: number, cache?: ICache, sig_sc?: string, nsig_sc?: string): Promise { + const player = new Player(player_id, sig_timestamp, sig_sc, nsig_sc); await player.cache(cache); return player; } async cache(cache?: ICache): Promise { - if (!cache) return; + if (!cache || !this.sig_sc || !this.nsig_sc) + return; const encoder = new TextEncoder(); @@ -219,21 +220,11 @@ export default class Player { return `function descramble_sig(a) { a = a.split(""); let ${obj_name}={${functions}}${calls} return a.join("") } descramble_sig(sig);`; } - static extractNSigSourceCode(data: string): string { - let sc = getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}'); - - 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'); - - return 'function descramble_nsig(a) { return a; } descramble_nsig(nsig)'; + static extractNSigSourceCode(data: string): string | undefined { + const nsig_function = findFunction(data, { includes: 'enhanced_except' }); + if (nsig_function) { + return `${nsig_function.result} ${nsig_function.name}(nsig);`; + } } get url(): string { @@ -241,6 +232,6 @@ export default class Player { } static get LIBRARY_VERSION(): number { - return 10; + return 11; } -} \ No newline at end of file +} 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/managers/PlaylistManager.ts b/src/core/managers/PlaylistManager.ts index 7c1cc8f16..f565640a8 100644 --- a/src/core/managers/PlaylistManager.ts +++ b/src/core/managers/PlaylistManager.ts @@ -96,8 +96,9 @@ export default class PlaylistManager { * Removes videos from a given playlist. * @param playlist_id - The playlist ID. * @param video_ids - An array of video IDs to remove from the playlist. + * @param use_set_video_ids - Option to remove videos using set video IDs. */ - async removeVideos(playlist_id: string, video_ids: string[]): Promise<{ playlist_id: string; action_result: any }> { + async removeVideos(playlist_id: string, video_ids: string[], use_set_video_ids = false): Promise<{ playlist_id: string; action_result: any }> { throwIfMissing({ playlist_id, video_ids }); if (!this.#actions.session.logged_in) @@ -115,7 +116,8 @@ export default class PlaylistManager { const payload: EditPlaylistEndpointOptions = { playlist_id, actions: [] }; const getSetVideoIds = async (pl: Feed): Promise => { - const videos = pl.videos.filter((video) => video_ids.includes(video.key('id').string())); + const key_id = use_set_video_ids ? 'set_video_id' : 'id'; + const videos = pl.videos.filter((video) => video_ids.includes(video.key(key_id).string())); videos.forEach((video) => payload.actions.push({ 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..002f584e2 100644 --- a/src/parser/classes/CompactVideo.ts +++ b/src/parser/classes/CompactVideo.ts @@ -1,5 +1,5 @@ import { timeToSeconds } from '../../utils/Utils.js'; -import { YTNode, type ObservedArray, type SuperParsedResult } from '../helpers.js'; +import { YTNode, type ObservedArray } from '../helpers.js'; import { Parser, type RawNode } from '../index.js'; import Menu from './menus/Menu.js'; import MetadataBadge from './MetadataBadge.js'; @@ -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/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/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