diff --git a/.eslintrc.yml b/.eslintrc.yml index 97a8660b..bf5fabb9 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -79,6 +79,7 @@ rules: prefer-template: error keyword-spacing: ["error", { "before": true } ] + object-curly-spacing: ["warn", "always"] array-bracket-spacing: ["error", "always"] arrow-parens: ["error", "always"] comma-dangle: ["error", "never"] diff --git a/src/Innertube.ts b/src/Innertube.ts index 2252fd00..151e39be 100644 --- a/src/Innertube.ts +++ b/src/Innertube.ts @@ -14,6 +14,7 @@ import NotificationsMenu from './parser/youtube/NotificationsMenu.js'; import Playlist from './parser/youtube/Playlist.js'; import Search from './parser/youtube/Search.js'; import VideoInfo from './parser/youtube/VideoInfo.js'; +import ShortsVideoInfo from './parser/ytshorts/VideoInfo.js'; import { Kids, Music, Studio } from './core/clients/index.js'; import { AccountManager, InteractionManager, PlaylistManager } from './core/managers/index.js'; @@ -30,7 +31,8 @@ import { NextEndpoint, PlayerEndpoint, ResolveURLEndpoint, - SearchEndpoint + SearchEndpoint, + Reel } from './core/endpoints/index.js'; import { GetUnseenCountEndpoint } from './core/endpoints/notification/index.js'; @@ -39,6 +41,7 @@ import type { ApiResponse } from './core/Actions.js'; import { type IBrowseResponse, type IParsedResponse } from './parser/types/index.js'; import type { INextRequest } from './types/index.js'; import type { DownloadOptions, FormatOptions } from './types/FormatUtils.js'; +import { encodeReelSequence } from './proto/index.js'; export type InnertubeConfig = SessionOptions; @@ -131,6 +134,32 @@ 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 }); + + const watchResponse = this.actions.execute( + Reel.WatchEndpoint.PATH, Reel.WatchEndpoint.build({ + short_id: short_id, + client: client + }) + ); + + const sequenceResponse = this.actions.execute( + Reel.WatchSequenceEndpoint.PATH, Reel.WatchSequenceEndpoint.build({ + sequenceParams: encodeReelSequence(short_id) + }) + ); + + const response = await Promise.all([ watchResponse, sequenceResponse ]); + + return new ShortsVideoInfo(response, this.actions); + } + /** * Searches a given query. * @param query - The search query. diff --git a/src/core/Actions.ts b/src/core/Actions.ts index 3f43d980..96db02e2 100644 --- a/src/core/Actions.ts +++ b/src/core/Actions.ts @@ -16,7 +16,7 @@ export interface ApiResponse { data: IRawResponse; } -export type InnertubeEndpoint = '/player' | '/search' | '/browse' | '/next' | '/updated_metadata' | '/notification/get_notification_menu' | string; +export type InnertubeEndpoint = '/player' | '/search' | '/browse' | '/next' | '/reel' | '/updated_metadata' | '/notification/get_notification_menu' | string; export type ParsedResponse = T extends '/player' ? IPlayerResponse : diff --git a/src/core/endpoints/index.ts b/src/core/endpoints/index.ts index ad859960..2ef93302 100644 --- a/src/core/endpoints/index.ts +++ b/src/core/endpoints/index.ts @@ -15,5 +15,6 @@ export * as Music from './music/index.js'; export * as Notification from './notification/index.js'; export * as Playlist from './playlist/index.js'; export * as Subscription from './subscription/index.js'; +export * as Reel from './reel/index.js'; export * as Upload from './upload/index.js'; export * as Kids from './kids/index.js'; \ No newline at end of file diff --git a/src/core/endpoints/reel/WatchEndpoint.ts b/src/core/endpoints/reel/WatchEndpoint.ts new file mode 100644 index 00000000..647ec422 --- /dev/null +++ b/src/core/endpoints/reel/WatchEndpoint.ts @@ -0,0 +1,18 @@ +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/WatchSequenceEndpoint.ts b/src/core/endpoints/reel/WatchSequenceEndpoint.ts new file mode 100644 index 00000000..7deb8982 --- /dev/null +++ b/src/core/endpoints/reel/WatchSequenceEndpoint.ts @@ -0,0 +1,14 @@ +import type { IReelSequenceRequest, ReelWatchSequenceEndpointOptions } from '../../../types/index.js'; + +export const PATH = '/reel/reel_watch_sequence'; + +/** + * Builds a `/reel/reel_watch_sequence` request payload. + * @param opts - The options to use. + * @returns The payload. + */ +export function build(opts: ReelWatchSequenceEndpointOptions): IReelSequenceRequest { + return { + sequenceParams: opts.sequenceParams + }; +} \ No newline at end of file diff --git a/src/core/endpoints/reel/index.ts b/src/core/endpoints/reel/index.ts new file mode 100644 index 00000000..4953dbbb --- /dev/null +++ b/src/core/endpoints/reel/index.ts @@ -0,0 +1,2 @@ +export * as WatchEndpoint from './WatchEndpoint.js'; +export * as WatchSequenceEndpoint from './WatchSequenceEndpoint.js'; \ No newline at end of file diff --git a/src/parser/classes/Command.ts b/src/parser/classes/Command.ts new file mode 100644 index 00000000..e9359e7b --- /dev/null +++ b/src/parser/classes/Command.ts @@ -0,0 +1,14 @@ +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/PivotButton.ts b/src/parser/classes/PivotButton.ts new file mode 100644 index 00000000..3cfe85b7 --- /dev/null +++ b/src/parser/classes/PivotButton.ts @@ -0,0 +1,28 @@ +import { type RawNode } from '../index.js'; +import { YTNode } from '../helpers.js'; +import Thumbnail from './misc/Thumbnail.js'; +import NavigationEndpoint from './NavigationEndpoint.js'; +import Text from './misc/Text.js'; + +export default class PivotButton extends YTNode { + static type = 'PivotButton'; + + thumbnail: Thumbnail[]; + endpoint: NavigationEndpoint; + content_description: Text; + target_id: string; + sound_attribution_title: Text; + waveform_animation_style: string; + background_animation_style: string; + + constructor(data: RawNode) { + super(); + this.thumbnail = Thumbnail.fromResponse(data.thumbnail); + this.endpoint = new NavigationEndpoint(data.onClickCommand); + this.content_description = new Text(data.contentDescription); + this.target_id = data.targetId; + this.sound_attribution_title = new Text(data.soundAttributionTitle); + this.waveform_animation_style = data.waveformAnimationStyle; + this.background_animation_style = data.backgroundAnimationStyle; + } +} \ No newline at end of file diff --git a/src/parser/classes/ReelPlayerHeader.ts b/src/parser/classes/ReelPlayerHeader.ts new file mode 100644 index 00000000..9ec0eaf5 --- /dev/null +++ b/src/parser/classes/ReelPlayerHeader.ts @@ -0,0 +1,24 @@ +import { type RawNode } from '../index.js'; +import { YTNode } from '../helpers.js'; +import Thumbnail from './misc/Thumbnail.js'; +import Author from './misc/Author.js'; +import Text from './misc/Text.js'; + +export default class ReelPlayerHeader extends YTNode { + static type = 'ReelPlayerHeader'; + + reel_title_text: Text; + timestamp_text: Text; + channel_title_text: Text; + channel_thumbnail: Thumbnail[]; + author: Author; + + constructor(data: RawNode) { + super(); + this.reel_title_text = new Text(data.reelTitleText); + this.timestamp_text = new Text(data.timestampText); + this.channel_title_text = new Text(data.channelTitleText); + this.channel_thumbnail = Thumbnail.fromResponse(data.channelThumbnail); + this.author = new Author(data.channelNavigationEndpoint, undefined); + } +} \ No newline at end of file diff --git a/src/parser/classes/ReelPlayerOverlay.ts b/src/parser/classes/ReelPlayerOverlay.ts new file mode 100644 index 00000000..cda01cfb --- /dev/null +++ b/src/parser/classes/ReelPlayerOverlay.ts @@ -0,0 +1,39 @@ +import { Parser, type RawNode } from '../index.js'; +import { YTNode } from '../helpers.js'; +import Button from './Button.js'; +import Menu from './menus/Menu.js'; +import InfoPanelContainer from './InfoPanelContainer.js'; +import LikeButton from './LikeButton.js'; +import ReelPlayerHeader from './ReelPlayerHeader.js'; +import PivotButton from './PivotButton.js'; + +export default class ReelPlayerOverlay extends YTNode { + static type = 'ReelPlayerOverlay'; + + like_button: LikeButton | null; + reel_player_header_supported_renderers: ReelPlayerHeader | null; + menu: Menu | null; + next_item_button: Button | null; + prev_item_button: Button | null; + subscribe_button_renderer: Button | null; + style: string; + view_comments_button: Button | null; + share_button: Button | null; + pivot_button: PivotButton | null; + info_panel: InfoPanelContainer | null; + + constructor(data: RawNode) { + super(); + this.like_button = Parser.parseItem(data.likeButton, LikeButton); + this.reel_player_header_supported_renderers = Parser.parseItem(data.reelPlayerHeaderSupportedRenderers, ReelPlayerHeader); + 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.style = data.style; + this.view_comments_button = Parser.parseItem(data.viewCommentsButton, Button); + this.share_button = Parser.parseItem(data.shareButton, Button); + this.pivot_button = Parser.parseItem(data.pivotButton, PivotButton); + this.info_panel = Parser.parseItem(data.infoPanel, InfoPanelContainer); + } +} \ No newline at end of file diff --git a/src/parser/classes/misc/Author.ts b/src/parser/classes/misc/Author.ts index a052309c..02025225 100644 --- a/src/parser/classes/misc/Author.ts +++ b/src/parser/classes/misc/Author.ts @@ -1,5 +1,5 @@ import * as Constants from '../../../utils/Constants.js'; -import type { YTNode} from '../../helpers.js'; +import type { YTNode } from '../../helpers.js'; import { observe, type ObservedArray } from '../../helpers.js'; import { Parser, type RawNode } from '../../index.js'; import type NavigationEndpoint from '../NavigationEndpoint.js'; diff --git a/src/parser/continuations.ts b/src/parser/continuations.ts index 85f4c99a..2af98812 100644 --- a/src/parser/continuations.ts +++ b/src/parser/continuations.ts @@ -1,4 +1,4 @@ -import type { ObservedArray} from './helpers.js'; +import type { ObservedArray } from './helpers.js'; import { YTNode, observe } from './helpers.js'; import type { RawNode } from './index.js'; import { Thumbnail } from './misc.js'; @@ -209,3 +209,16 @@ export class LiveChatContinuation extends YTNode { this.viewer_name = data.viewerName; } } + +export class ContinuationCommand extends YTNode { + static readonly type = 'ContinuationCommand'; + + request: string; + token: string; + + constructor(data: RawNode) { + super(); + this.request = data.request; + this.token = data.token; + } +} \ No newline at end of file diff --git a/src/parser/index.ts b/src/parser/index.ts index 1a038206..7a71b5f4 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -6,5 +6,6 @@ export * as YTNodes from './nodes.js'; export * as YT from './youtube/index.js'; export * as YTMusic from './ytmusic/index.js'; export * as YTKids from './ytkids/index.js'; +export * as YTShorts from './ytshorts/index.js'; export * as Helpers from './helpers.js'; export * as Generator from './generator.js'; \ No newline at end of file diff --git a/src/parser/nodes.ts b/src/parser/nodes.ts index 92d713f2..41725da0 100644 --- a/src/parser/nodes.ts +++ b/src/parser/nodes.ts @@ -61,6 +61,7 @@ export { default as ClipCreationTextInput } from './classes/ClipCreationTextInpu export { default as ClipSection } from './classes/ClipSection.js'; export { default as CollaboratorInfoCardContent } from './classes/CollaboratorInfoCardContent.js'; export { default as CollageHeroImage } from './classes/CollageHeroImage.js'; +export { default as Command } from './classes/Command.js'; export { default as AuthorCommentBadge } from './classes/comments/AuthorCommentBadge.js'; export { default as Comment } from './classes/comments/Comment.js'; export { default as CommentActionButtons } from './classes/comments/CommentActionButtons.js'; @@ -261,6 +262,7 @@ export { default as Notification } from './classes/Notification.js'; export { default as PageHeader } from './classes/PageHeader.js'; export { default as PageHeaderView } from './classes/PageHeaderView.js'; export { default as PageIntroduction } from './classes/PageIntroduction.js'; +export { default as PivotButton } from './classes/PivotButton.js'; export { default as PlayerAnnotationsExpanded } from './classes/PlayerAnnotationsExpanded.js'; export { default as PlayerCaptionsTracklist } from './classes/PlayerCaptionsTracklist.js'; export { default as PlayerControlsOverlay } from './classes/PlayerControlsOverlay.js'; @@ -300,6 +302,8 @@ export { default as ProfileColumnUserInfo } from './classes/ProfileColumnUserInf export { default as Quiz } from './classes/Quiz.js'; export { default as RecognitionShelf } from './classes/RecognitionShelf.js'; export { default as ReelItem } from './classes/ReelItem.js'; +export { default as ReelPlayerHeader } from './classes/ReelPlayerHeader.js'; +export { default as ReelPlayerOverlay } from './classes/ReelPlayerOverlay.js'; export { default as ReelShelf } from './classes/ReelShelf.js'; export { default as RelatedChipCloud } from './classes/RelatedChipCloud.js'; export { default as RichGrid } from './classes/RichGrid.js'; diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 3ee24c95..73effdf2 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -27,7 +27,7 @@ import { Continuation, ItemSectionContinuation, SectionListContinuation, LiveChatContinuation, MusicPlaylistShelfContinuation, MusicShelfContinuation, GridContinuation, PlaylistPanelContinuation, NavigateAction, ShowMiniplayerCommand, - ReloadContinuationItemsCommand + ReloadContinuationItemsCommand, ContinuationCommand } from './continuations.js'; export type ParserError = { @@ -304,6 +304,14 @@ export function parseResponse(data: } _clearMemo(); + _createMemo(); + const entries = parse(data.entries); + if (entries) { + parsed_data.entries = entries; + parsed_data.entries_memo = _getMemo(); + } + _clearMemo(); + applyMutations(contents_memo, data.frameworkUpdates?.entityBatchUpdate?.mutations); const continuation = data.continuation ? parseC(data.continuation) : null; @@ -311,6 +319,11 @@ export function parseResponse(data: parsed_data.continuation = continuation; } + const continuationEndpoint = data.continuationEndpoint ? parseLC(data.continuationEndpoint) : null; + if (continuationEndpoint) { + parsed_data.continuationEndpoint = continuationEndpoint; + } + const metadata = parse(data.metadata); if (metadata) { parsed_data.metadata = metadata; @@ -577,6 +590,8 @@ export function parseLC(data: RawNode) { return new GridContinuation(data.gridContinuation); if (data.playlistPanelContinuation) return new PlaylistPanelContinuation(data.playlistPanelContinuation); + if (data.continuationCommand) + return new ContinuationCommand(data.continuationCommand); return null; } diff --git a/src/parser/types/ParsedResponse.ts b/src/parser/types/ParsedResponse.ts index 3bb82e25..c138a098 100644 --- a/src/parser/types/ParsedResponse.ts +++ b/src/parser/types/ParsedResponse.ts @@ -3,7 +3,7 @@ import type { Memo, ObservedArray, SuperParsedResult, YTNode } from '../helpers. import type { ReloadContinuationItemsCommand, Continuation, GridContinuation, ItemSectionContinuation, LiveChatContinuation, MusicPlaylistShelfContinuation, MusicShelfContinuation, - PlaylistPanelContinuation, SectionListContinuation + PlaylistPanelContinuation, SectionListContinuation, ContinuationCommand } from '../index.js'; import type PlayerCaptionsTracklist from '../classes/PlayerCaptionsTracklist.js'; @@ -41,7 +41,7 @@ export interface IParsedResponse { on_response_received_commands_memo?: Memo; continuation?: Continuation; continuation_contents?: ItemSectionContinuation | SectionListContinuation | LiveChatContinuation | MusicPlaylistShelfContinuation | - MusicShelfContinuation | GridContinuation | PlaylistPanelContinuation; + MusicShelfContinuation | GridContinuation | PlaylistPanelContinuation | ContinuationCommand; continuation_contents_memo?: Memo; metadata?: SuperParsedResult; microformat?: YTNode; @@ -66,6 +66,9 @@ export interface IParsedResponse { cards?: CardCollection; engagement_panels?: ObservedArray; items?: SuperParsedResult; + entries?: SuperParsedResult; + entries_memo?: Memo; + continuationEndpoint?: YTNode; } export interface IStreamingData { diff --git a/src/parser/ytshorts/VideoInfo.ts b/src/parser/ytshorts/VideoInfo.ts new file mode 100644 index 00000000..140ca097 --- /dev/null +++ b/src/parser/ytshorts/VideoInfo.ts @@ -0,0 +1,58 @@ +import type NavigationEndpoint from '../classes/NavigationEndpoint.js'; +import type PlayerOverlay from '../classes/PlayerOverlay.js'; +import type Actions from '../../core/Actions.js'; +import type { ApiResponse } from '../../core/Actions.js'; +import type { ObservedArray, YTNode } from '../helpers.js'; +import { Parser, ContinuationCommand } from '../index.js'; +import { InnertubeError } from '../../utils/Utils.js'; +import { + Reel +} from '../../core/endpoints/index.js'; + +class VideoInfo { + #watch_next_continuation?: ContinuationCommand; + #actions: Actions; + + basic_info; + watch_next_feed?: ObservedArray; + current_video_endpoint?: NavigationEndpoint; + player_overlays?: PlayerOverlay; + + constructor(data: [ApiResponse, ApiResponse], actions: Actions) { + this.#actions = actions; + + const info = Parser.parseResponse(data[0].data); + + const watchNext = Parser.parseResponse(data[1].data); + + this.basic_info = info.video_details; + + this.watch_next_feed = watchNext.entries?.array(); + this.#watch_next_continuation = watchNext.continuationEndpoint?.as(ContinuationCommand); + } + + /** + * Retrieves watch next feed continuation. + */ + async getWatchNextContinuation(): Promise { + if (!this.#watch_next_continuation) + throw new InnertubeError('Watch next feed continuation not found'); + + const response = await this.#actions.execute(Reel.WatchSequenceEndpoint.PATH, Reel.WatchSequenceEndpoint.build({ + sequenceParams: this.#watch_next_continuation.token + })); + + if (!response.success) { + throw new InnertubeError('Continue failed ', response.status_code); + } + + const parsed = Parser.parseResponse(response.data); + + this.watch_next_feed = parsed.entries?.array(); + this.#watch_next_continuation = parsed.continuationEndpoint?.as(ContinuationCommand); + + return this; + } +} + +export default VideoInfo; \ No newline at end of file diff --git a/src/parser/ytshorts/index.ts b/src/parser/ytshorts/index.ts new file mode 100644 index 00000000..245ce2e5 --- /dev/null +++ b/src/parser/ytshorts/index.ts @@ -0,0 +1 @@ +export { default as VideoInfo } from './VideoInfo.js'; \ No newline at end of file diff --git a/src/proto/generated/messages/youtube/ReelSequence.ts b/src/proto/generated/messages/youtube/ReelSequence.ts new file mode 100644 index 00000000..9a6ea41d --- /dev/null +++ b/src/proto/generated/messages/youtube/ReelSequence.ts @@ -0,0 +1,134 @@ +import { + Type as Params, + encodeJson as encodeJson_1, + decodeJson as decodeJson_1, + encodeBinary as encodeBinary_1, + decodeBinary as decodeBinary_1, +} from "./(ReelSequence)/Params.js"; +import { + tsValueToJsonValueFns, + jsonValueToTsValueFns, +} from "../../runtime/json/scalar.js"; +import { + WireMessage, + WireType, +} from "../../runtime/wire/index.js"; +import { + default as serialize, +} from "../../runtime/wire/serialize.js"; +import { + tsValueToWireValueFns, + wireValueToTsValueFns, +} from "../../runtime/wire/scalar.js"; +import { + default as deserialize, +} from "../../runtime/wire/deserialize.js"; + +export declare namespace $.youtube { + export type ReelSequence = { + shortId: string; + params?: Params; + feature2: number; + feature3: number; + } +} + +export type Type = $.youtube.ReelSequence; + +export function getDefaultValue(): $.youtube.ReelSequence { + return { + shortId: "", + params: undefined, + feature2: 0, + feature3: 0, + }; +} + +export function createValue(partialValue: Partial<$.youtube.ReelSequence>): $.youtube.ReelSequence { + return { + ...getDefaultValue(), + ...partialValue, + }; +} + +export function encodeJson(value: $.youtube.ReelSequence): unknown { + const result: any = {}; + if (value.shortId !== undefined) result.shortId = tsValueToJsonValueFns.string(value.shortId); + if (value.params !== undefined) result.params = encodeJson_1(value.params); + if (value.feature2 !== undefined) result.feature2 = tsValueToJsonValueFns.int32(value.feature2); + if (value.feature3 !== undefined) result.feature3 = tsValueToJsonValueFns.int32(value.feature3); + return result; +} + +export function decodeJson(value: any): $.youtube.ReelSequence { + const result = getDefaultValue(); + if (value.shortId !== undefined) result.shortId = jsonValueToTsValueFns.string(value.shortId); + if (value.params !== undefined) result.params = decodeJson_1(value.params); + if (value.feature2 !== undefined) result.feature2 = jsonValueToTsValueFns.int32(value.feature2); + if (value.feature3 !== undefined) result.feature3 = jsonValueToTsValueFns.int32(value.feature3); + return result; +} + +export function encodeBinary(value: $.youtube.ReelSequence): Uint8Array { + const result: WireMessage = []; + if (value.shortId !== undefined) { + const tsValue = value.shortId; + result.push( + [1, tsValueToWireValueFns.string(tsValue)], + ); + } + if (value.params !== undefined) { + const tsValue = value.params; + result.push( + [5, { type: WireType.LengthDelimited as const, value: encodeBinary_1(tsValue) }], + ); + } + if (value.feature2 !== undefined) { + const tsValue = value.feature2; + result.push( + [10, tsValueToWireValueFns.int32(tsValue)], + ); + } + if (value.feature3 !== undefined) { + const tsValue = value.feature3; + result.push( + [13, tsValueToWireValueFns.int32(tsValue)], + ); + } + return serialize(result); +} + +export function decodeBinary(binary: Uint8Array): $.youtube.ReelSequence { + const result = getDefaultValue(); + const wireMessage = deserialize(binary); + const wireFields = new Map(wireMessage); + field: { + const wireValue = wireFields.get(1); + if (wireValue === undefined) break field; + const value = wireValueToTsValueFns.string(wireValue); + if (value === undefined) break field; + result.shortId = value; + } + field: { + const wireValue = wireFields.get(5); + if (wireValue === undefined) break field; + const value = wireValue.type === WireType.LengthDelimited ? decodeBinary_1(wireValue.value) : undefined; + if (value === undefined) break field; + result.params = value; + } + field: { + const wireValue = wireFields.get(10); + if (wireValue === undefined) break field; + const value = wireValueToTsValueFns.int32(wireValue); + if (value === undefined) break field; + result.feature2 = value; + } + field: { + const wireValue = wireFields.get(13); + if (wireValue === undefined) break field; + const value = wireValueToTsValueFns.int32(wireValue); + if (value === undefined) break field; + result.feature3 = value; + } + return result; +} diff --git a/src/proto/generated/messages/youtube/index.ts b/src/proto/generated/messages/youtube/index.ts index c22453b1..74983538 100644 --- a/src/proto/generated/messages/youtube/index.ts +++ b/src/proto/generated/messages/youtube/index.ts @@ -10,3 +10,4 @@ export type { Type as PeformCommentActionParams } from "./PeformCommentActionPar export type { Type as MusicSearchFilter } from "./MusicSearchFilter.js"; export type { Type as SearchFilter } from "./SearchFilter.js"; export type { Type as Hashtag } from "./Hashtag.js"; +export type { Type as ReelSequence } from "./ReelSequence.js"; diff --git a/src/proto/index.ts b/src/proto/index.ts index 6dcc3c7a..e1a030a2 100644 --- a/src/proto/index.ts +++ b/src/proto/index.ts @@ -14,6 +14,7 @@ import * as PeformCommentActionParams from './generated/messages/youtube/PeformC import * as NotificationPreferences from './generated/messages/youtube/NotificationPreferences.js'; import * as InnertubePayload from './generated/messages/youtube/InnertubePayload.js'; import * as Hashtag from './generated/messages/youtube/Hashtag.js'; +import * as ReelSequence from './generated/messages/youtube/ReelSequence.js'; export function encodeVisitorData(id: string, timestamp: number): string { const buf = VisitorData.encodeBinary({ id, timestamp }); @@ -327,5 +328,17 @@ export function encodeHashtag(hashtag: string): string { } }); + return encodeURIComponent(u8ToBase64(buf)); +} + +export function encodeReelSequence(short_id: string): string { + const buf = ReelSequence.encodeBinary({ + shortId: short_id, + params: { + number: 5 + }, + feature2: 25, + feature3: 0 + }); return encodeURIComponent(u8ToBase64(buf)); } \ No newline at end of file diff --git a/src/proto/youtube.proto b/src/proto/youtube.proto index 9b97ed3b..9b2f79ca 100644 --- a/src/proto/youtube.proto +++ b/src/proto/youtube.proto @@ -264,4 +264,15 @@ message Hashtag { } required Params params = 93; +} + +message ReelSequence { + required string short_id = 1; + message Params { + required int32 number = 3; + } + + required Params params = 5; + required int32 feature_2 = 10; + required int32 feature_3 = 13; } \ No newline at end of file diff --git a/src/types/Endpoints.ts b/src/types/Endpoints.ts index 124d6ec5..c59ce7ca 100644 --- a/src/types/Endpoints.ts +++ b/src/types/Endpoints.ts @@ -362,4 +362,42 @@ export type IBlocklistPickerRequest = { blockedForKidsContent: { external_channel_id: string; } +} + +export interface IReelWatchRequest { + playerRequest: { + videoId: string, + params: string, + }, + params?: string; +} + +export type ReelWatchEndpointOptions = { + /** + * The shorts ID. + */ + short_id: string; + /** + * The client to use. + */ + client?: InnerTubeClient; + /** + * Additional protobuf parameters. + */ + params?: string; +} + +export interface IReelSequenceRequest { + sequenceParams: string; +} + +export type ReelWatchSequenceEndpointOptions = { + /** + * The protobuf parameters. + */ + sequenceParams: string; + /** + * The client to use. + */ + client?: InnerTubeClient; } \ No newline at end of file diff --git a/test/main.test.ts b/test/main.test.ts index 800dec43..ddb8a03c 100644 --- a/test/main.test.ts +++ b/test/main.test.ts @@ -19,6 +19,22 @@ describe('YouTube.js Tests', () => { expect(info.basic_info.id).toBe('bUHZ2k9DYHY'); }); + test('Innertube#getShortsWatchItem', async () => { + const info = await innertube.getShortsWatchItem('jOydBrmmjfk'); + expect(info.watch_next_feed?.length).toBeGreaterThan(0); + }); + + test('Innertube#getShortsWatchItem#Continue', async () => { + const info = await innertube.getShortsWatchItem('jOydBrmmjfk'); + expect(info.watch_next_feed?.length).toBeGreaterThan(0); + const previousData = info.watch_next_feed?.map(value => value.as(YTNodes.Command).endpoint) + const cont = await info.getWatchNextContinuation() + + expect(cont.watch_next_feed?.length).toBeGreaterThan(0); + const newData = cont.watch_next_feed?.map(value => value.as(YTNodes.Command).endpoint) + expect(previousData).not.toEqual(newData) + }); + describe('Innertube#getBasicInfo', () => { test('Format#language multiple audio tracks', async () => { const info = await innertube.getBasicInfo('Kn56bMZ9OE8')