diff --git a/src/core/Session.ts b/src/core/Session.ts index 7ec7162ef..ab11c71a5 100644 --- a/src/core/Session.ts +++ b/src/core/Session.ts @@ -33,25 +33,25 @@ export interface Context { hl: string; gl: string; remoteHost?: string; - screenDensityFloat: number; - screenHeightPoints: number; - screenPixelDensity: number; - screenWidthPoints: number; - visitorData: string; + screenDensityFloat?: number; + screenHeightPoints?: number; + screenPixelDensity?: number; + screenWidthPoints?: number; + visitorData?: string; clientName: string; clientVersion: string; clientScreen?: string, - androidSdkVersion?: string; + androidSdkVersion?: number; osName: string; osVersion: string; platform: string; clientFormFactor: string; - userInterfaceTheme: string; + userInterfaceTheme?: string; timeZone: string; userAgent?: string; browserName?: string; browserVersion?: string; - originalUrl: string; + originalUrl?: string; deviceMake: string; deviceModel: string; utcOffsetMinutes: number; @@ -73,6 +73,10 @@ export interface Context { thirdParty?: { embedUrl: string; }; + request?: { + useSsl: boolean; + internalExperimentFlags: any[]; + }; } export interface SessionOptions { @@ -327,11 +331,17 @@ export default class Session extends EventEmitter { }, user: { enableSafetyMode: options.enable_safety_mode, - lockedSafetyMode: false, - onBehalfOfUser: options.on_behalf_of_user + lockedSafetyMode: false + }, + request: { + useSsl: true, + internalExperimentFlags: [] } }; + if (options.on_behalf_of_user) + context.user.onBehalfOfUser = options.on_behalf_of_user; + return { context, api_key, api_version }; } @@ -366,11 +376,17 @@ export default class Session extends EventEmitter { }, user: { enableSafetyMode: options.enable_safety_mode, - lockedSafetyMode: false, - onBehalfOfUser: options.on_behalf_of_user + lockedSafetyMode: false + }, + request: { + useSsl: true, + internalExperimentFlags: [] } }; + if (options.on_behalf_of_user) + context.user.onBehalfOfUser = options.on_behalf_of_user; + return { context, api_key: Constants.CLIENTS.WEB.API_KEY, api_version: Constants.CLIENTS.WEB.API_VERSION }; } diff --git a/src/core/endpoints/PlayerEndpoint.ts b/src/core/endpoints/PlayerEndpoint.ts index 5cff0a558..f8d0d0456 100644 --- a/src/core/endpoints/PlayerEndpoint.ts +++ b/src/core/endpoints/PlayerEndpoint.ts @@ -1,3 +1,4 @@ +import { encodeShortsParam } from '../../proto/index.js'; import type { IPlayerRequest, PlayerEndpointOptions } from '../../types/index.js'; export const PATH = '/player'; @@ -43,7 +44,7 @@ export function build(opts: PlayerEndpointOptions): IPlayerRequest { client: opts.client, playlistId: opts.playlist_id, // Workaround streaming URLs returning 403 or getting throttled when using Android based clients. - params: is_android ? '2AMBCgIQBg' : opts.params + params: is_android ? encodeShortsParam() : opts.params } }; } \ No newline at end of file diff --git a/src/proto/generated/messages/youtube/(ShortsParam)/Field1.ts b/src/proto/generated/messages/youtube/(ShortsParam)/Field1.ts new file mode 100644 index 000000000..9a687f2cb --- /dev/null +++ b/src/proto/generated/messages/youtube/(ShortsParam)/Field1.ts @@ -0,0 +1,75 @@ +import { + tsValueToJsonValueFns, + jsonValueToTsValueFns, +} from "../../../runtime/json/scalar.js"; +import { + WireMessage, +} 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.ShortsParam { + export type Field1 = { + p1: number; + } +} + +export type Type = $.youtube.ShortsParam.Field1; + +export function getDefaultValue(): $.youtube.ShortsParam.Field1 { + return { + p1: 0, + }; +} + +export function createValue(partialValue: Partial<$.youtube.ShortsParam.Field1>): $.youtube.ShortsParam.Field1 { + return { + ...getDefaultValue(), + ...partialValue, + }; +} + +export function encodeJson(value: $.youtube.ShortsParam.Field1): unknown { + const result: any = {}; + if (value.p1 !== undefined) result.p1 = tsValueToJsonValueFns.int32(value.p1); + return result; +} + +export function decodeJson(value: any): $.youtube.ShortsParam.Field1 { + const result = getDefaultValue(); + if (value.p1 !== undefined) result.p1 = jsonValueToTsValueFns.int32(value.p1); + return result; +} + +export function encodeBinary(value: $.youtube.ShortsParam.Field1): Uint8Array { + const result: WireMessage = []; + if (value.p1 !== undefined) { + const tsValue = value.p1; + result.push( + [1, tsValueToWireValueFns.int32(tsValue)], + ); + } + return serialize(result); +} + +export function decodeBinary(binary: Uint8Array): $.youtube.ShortsParam.Field1 { + 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.int32(wireValue); + if (value === undefined) break field; + result.p1 = value; + } + return result; +} diff --git a/src/proto/generated/messages/youtube/(ShortsParam)/index.ts b/src/proto/generated/messages/youtube/(ShortsParam)/index.ts new file mode 100644 index 000000000..9091623bd --- /dev/null +++ b/src/proto/generated/messages/youtube/(ShortsParam)/index.ts @@ -0,0 +1 @@ +export type { Type as Field1 } from "./Field1.js"; diff --git a/src/proto/generated/messages/youtube/ShortsParam.ts b/src/proto/generated/messages/youtube/ShortsParam.ts new file mode 100644 index 000000000..01cf4c89f --- /dev/null +++ b/src/proto/generated/messages/youtube/ShortsParam.ts @@ -0,0 +1,100 @@ +import { + Type as Field1, + encodeJson as encodeJson_1, + decodeJson as decodeJson_1, + encodeBinary as encodeBinary_1, + decodeBinary as decodeBinary_1, +} from "./(ShortsParam)/Field1.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 ShortsParam = { + f1?: Field1; + p59: number; + } +} + +export type Type = $.youtube.ShortsParam; + +export function getDefaultValue(): $.youtube.ShortsParam { + return { + f1: undefined, + p59: 0, + }; +} + +export function createValue(partialValue: Partial<$.youtube.ShortsParam>): $.youtube.ShortsParam { + return { + ...getDefaultValue(), + ...partialValue, + }; +} + +export function encodeJson(value: $.youtube.ShortsParam): unknown { + const result: any = {}; + if (value.f1 !== undefined) result.f1 = encodeJson_1(value.f1); + if (value.p59 !== undefined) result.p59 = tsValueToJsonValueFns.int32(value.p59); + return result; +} + +export function decodeJson(value: any): $.youtube.ShortsParam { + const result = getDefaultValue(); + if (value.f1 !== undefined) result.f1 = decodeJson_1(value.f1); + if (value.p59 !== undefined) result.p59 = jsonValueToTsValueFns.int32(value.p59); + return result; +} + +export function encodeBinary(value: $.youtube.ShortsParam): Uint8Array { + const result: WireMessage = []; + if (value.f1 !== undefined) { + const tsValue = value.f1; + result.push( + [1, { type: WireType.LengthDelimited as const, value: encodeBinary_1(tsValue) }], + ); + } + if (value.p59 !== undefined) { + const tsValue = value.p59; + result.push( + [59, tsValueToWireValueFns.int32(tsValue)], + ); + } + return serialize(result); +} + +export function decodeBinary(binary: Uint8Array): $.youtube.ShortsParam { + 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 = wireValue.type === WireType.LengthDelimited ? decodeBinary_1(wireValue.value) : undefined; + if (value === undefined) break field; + result.f1 = value; + } + field: { + const wireValue = wireFields.get(59); + if (wireValue === undefined) break field; + const value = wireValueToTsValueFns.int32(wireValue); + if (value === undefined) break field; + result.p59 = value; + } + return result; +} diff --git a/src/proto/generated/messages/youtube/index.ts b/src/proto/generated/messages/youtube/index.ts index 74983538c..6c4524d13 100644 --- a/src/proto/generated/messages/youtube/index.ts +++ b/src/proto/generated/messages/youtube/index.ts @@ -11,3 +11,4 @@ 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"; +export type { Type as ShortsParam } from "./ShortsParam.js"; diff --git a/src/proto/index.ts b/src/proto/index.ts index 2e2c27152..c7aae08cb 100644 --- a/src/proto/index.ts +++ b/src/proto/index.ts @@ -15,6 +15,7 @@ import * as NotificationPreferences from './generated/messages/youtube/Notificat 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'; +import * as ShortsParam from './generated/messages/youtube/ShortsParam.js'; export function encodeVisitorData(id: string, timestamp: number): string { const buf = VisitorData.encodeBinary({ id, timestamp }); @@ -341,4 +342,14 @@ export function encodeReelSequence(short_id: string): string { feature3: 0 }); return encodeURIComponent(u8ToBase64(buf)); +} + +export function encodeShortsParam() { + const buf = ShortsParam.encodeBinary({ + f1: { + p1: 1 + }, + p59: 1 + }); + return encodeURIComponent(u8ToBase64(buf)); } \ No newline at end of file diff --git a/src/proto/youtube.proto b/src/proto/youtube.proto index 9b2f79caf..b9a130193 100644 --- a/src/proto/youtube.proto +++ b/src/proto/youtube.proto @@ -275,4 +275,12 @@ message ReelSequence { required Params params = 5; required int32 feature_2 = 10; required int32 feature_3 = 13; +} + +message ShortsParam { + message Field1 { + int32 p1 = 1; + } + Field1 f1 = 1; + int32 p59 = 59; } \ No newline at end of file diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index 869d21061..0f4c1ebf6 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -56,9 +56,9 @@ export const CLIENTS = Object.freeze({ }, ANDROID: { NAME: 'ANDROID', - VERSION: '18.06.35', - SDK_VERSION: '29', - USER_AGENT: 'com.google.android.youtube/18.06.35 (Linux; U; Android 10; US)' + VERSION: '18.48.37', + SDK_VERSION: 33, + USER_AGENT: 'com.google.android.youtube/18.48.37(Linux; U; Android 13; en_US; sdk_gphone64_x86_64 Build/UPB4.230623.005) gzip' }, YTSTUDIO_ANDROID: { NAME: 'ANDROID_CREATOR', diff --git a/src/utils/HTTPClient.ts b/src/utils/HTTPClient.ts index 57f4d64f4..5a027f8fe 100644 --- a/src/utils/HTTPClient.ts +++ b/src/utils/HTTPClient.ts @@ -95,6 +95,7 @@ export default class HTTPClient { if (Platform.shim.server) { if (n_body.context.client.clientName === 'ANDROID' || n_body.context.client.clientName === 'ANDROID_MUSIC') { request_headers.set('User-Agent', Constants.CLIENTS.ANDROID.USER_AGENT); + request_headers.set('X-GOOG-API-FORMAT-VERSION', '2'); } else if (n_body.context.client.clientName === 'iOS') { request_headers.set('User-Agent', Constants.CLIENTS.iOS.USER_AGENT); } @@ -102,6 +103,14 @@ export default class HTTPClient { is_web_kids = n_body.context.client.clientName === 'WEB_KIDS'; request_body = JSON.stringify(n_body); + } else if (content_type === 'application/x-protobuf') { + // Assume it is always an Android request. + if (Platform.shim.server) { + request_headers.set('User-Agent', Constants.CLIENTS.ANDROID.USER_AGENT); + request_headers.set('X-GOOG-API-FORMAT-VERSION', '2'); + request_headers.delete('X-Youtube-Client-Version'); + request_headers.delete('X-Origin'); + } } // Authenticate (NOTE: YouTube Kids does not support regular bearer tokens) @@ -152,7 +161,7 @@ export default class HTTPClient { ctx.client.androidSdkVersion = Constants.CLIENTS.ANDROID.SDK_VERSION; ctx.client.userAgent = Constants.CLIENTS.ANDROID.USER_AGENT; ctx.client.osName = 'Android'; - ctx.client.osVersion = '10'; + ctx.client.osVersion = '13'; ctx.client.platform = 'MOBILE'; } diff --git a/test/main.test.ts b/test/main.test.ts index fdc11a49e..8e7b7c5b0 100644 --- a/test/main.test.ts +++ b/test/main.test.ts @@ -19,6 +19,11 @@ describe('YouTube.js Tests', () => { expect(info.basic_info.id).toBe('bUHZ2k9DYHY'); }); + test('Innertube#getBasicInfo (Android)', async () => { + const info = await innertube.getBasicInfo('ksEYRaIpP7A'); + expect(info.basic_info.id).toBe('ksEYRaIpP7A'); + }); + test('Innertube#getShortsWatchItem', async () => { const info = await innertube.getShortsWatchItem('jOydBrmmjfk'); expect(info.watch_next_feed?.length).toBeGreaterThan(0); @@ -94,7 +99,7 @@ describe('YouTube.js Tests', () => { let comments: YT.Comments; beforeAll(async () => { - comments = await innertube.getComments('bUHZ2k9DYHY'); + comments = await innertube.getComments('gmX-ceF-N1k'); expect(comments).toBeDefined(); expect(comments.header).toBeDefined(); expect(comments.contents).toBeDefined(); @@ -107,20 +112,10 @@ describe('YouTube.js Tests', () => { expect(incremental_continuation.contents.length).toBeGreaterThan(0); }); - describe('CommentThread#getReplies', () => { - let loaded_comment_thread: YTNodes.CommentThread; - - beforeAll(async () => { - let comment_thread = comments.contents.first(); - loaded_comment_thread = await comment_thread.getReplies(); - expect(loaded_comment_thread.replies).toBeDefined(); - }); - - test('CommentThread#getContinuation', async () => { - const incremental_continuation = await loaded_comment_thread.getContinuation(); - expect(incremental_continuation.replies).toBeDefined(); - expect(incremental_continuation.replies?.length).toBeGreaterThan(0); - }); + test('CommentThread#getReplies', async () => { + let comment_thread = comments.contents.first(); + let loaded_comment_thread = await comment_thread.getReplies(); + expect(loaded_comment_thread.replies).toBeDefined(); }); });