diff --git a/examples/youtube-studio-web/package.json b/examples/youtube-studio-web/package.json new file mode 100644 index 0000000000..f20be0025c --- /dev/null +++ b/examples/youtube-studio-web/package.json @@ -0,0 +1,17 @@ +{ + "name": "youtube-studio-web", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": {}, + "devDependencies": { + "@types/jsdom": "^27.0.0", + "typescript": "^5.0.4" + }, + "dependencies": { + "bgutils-js": "^4.0.3", + "cookie-eater": "^1.0.0", + "jsdom": "^28.1.0", + "youtubei.js": "file:../../" + } +} diff --git a/examples/youtube-studio-web/src/GetSessionToken.ts b/examples/youtube-studio-web/src/GetSessionToken.ts new file mode 100644 index 0000000000..d537e6d1ba --- /dev/null +++ b/examples/youtube-studio-web/src/GetSessionToken.ts @@ -0,0 +1,16 @@ +import Innertube, { UniversalCache } from 'youtubei.js'; +import { botguard_solver, get_channel_id, youtube_cookies } from './utils.ts'; + + +(async () => { + const COOKIES = await youtube_cookies(); + + const yt = await Innertube.create({ cache: new UniversalCache(false), cookie: COOKIES }); + + const CHANNEL_ID = await get_channel_id(yt); + + const yt_studio_web = yt.studioWeb(CHANNEL_ID); + yt_studio_web.setBotGuardSolver(botguard_solver); + const session_token = await yt_studio_web.getSessionToken(); + console.log(session_token); +})(); \ No newline at end of file diff --git a/examples/youtube-studio-web/src/UploadVideo.ts b/examples/youtube-studio-web/src/UploadVideo.ts new file mode 100644 index 0000000000..00878d1d04 --- /dev/null +++ b/examples/youtube-studio-web/src/UploadVideo.ts @@ -0,0 +1,40 @@ +import Innertube, { UniversalCache } from 'youtubei.js'; +import { botguard_solver, get_channel_id, get_file_named_buffer_base64, get_file_named_buffer_reader } from "./utils.ts"; + +const COOKIES = ""; // ?? Place your YouTube cookies here +const VIDEO_FILE_PATH = ""; // ?? Place your video file path here +const THUMBNAIL_FILE_PATH = ""; // ?? Place your thumbnail file path here +const SRT_FILE_PATH = ""; // ?? Place your srt file path here + +(async () => { + const yt = await Innertube.create({ cache: new UniversalCache(false), cookie: COOKIES }); + + const CHANNEL_ID = await get_channel_id(yt); + + const yt_studio_web = yt.studioWeb(CHANNEL_ID); + yt_studio_web.setBotGuardSolver(botguard_solver); + + console.log('creating...'); + + const created_response = await yt_studio_web.uploadVideo(await get_file_named_buffer_reader(VIDEO_FILE_PATH), { + title: "This is a test upload", + thumbnail: await get_file_named_buffer_reader(THUMBNAIL_FILE_PATH), + subtitles: { + synced: true, + data: await get_file_named_buffer_base64(SRT_FILE_PATH) + }, + description: "This is a test description", + visibility: "PRIVATE" + }, (written_bytes, total_bytes) => { + // NOTE this callback only fires around every 100mb + console.log(`${(written_bytes / total_bytes) * 100}%`); + }, async (full_created) => { + // NOTE this cycle fires every 2s-ish + await yt_studio_web.uploadFeedbackCycle([full_created.feedback_token], (content) => { + if(content[0].transfer_progress_bar?.progress_message) console.log(content[0].transfer_progress_bar?.progress_message); + if(content[0].is_processing) console.log("processing..."); + return true; + }); + }); + console.log(created_response); +})(); \ No newline at end of file diff --git a/examples/youtube-studio-web/src/utils.ts b/examples/youtube-studio-web/src/utils.ts new file mode 100644 index 0000000000..ff0c9b14d8 --- /dev/null +++ b/examples/youtube-studio-web/src/utils.ts @@ -0,0 +1,85 @@ +import { BotGuardClient } from "bgutils-js/botguard"; +import { USER_AGENT } from "bgutils-js/utils"; +import { JSDOM, VirtualConsole } from "jsdom"; +import Innertube, { type Types } from 'youtubei.js'; +import path from "path"; +import fs from 'fs/promises'; +import { get_cookies } from 'cookie-eater'; + +export const botguard_solver: Types.BotGuardSolver = { + solve: async(botguard_challenge, binding) => { + const virtual_console = new VirtualConsole(); + const dom = new JSDOM('', { url: "https://www.youtube.com", referrer: "https://www.youtube.com/", userAgent: USER_AGENT, resources: "usable", runScripts: "dangerously", virtualConsole: virtual_console }); + + Object.assign(globalThis, { window: dom.window, document: dom.window.document, location: dom.window.location, origin: dom.window.origin }); + + if (!("navigator" in globalThis)) { + Object.defineProperty(globalThis, "navigator", { value: dom.window.navigator }); + } + + Object.defineProperty(dom.window.HTMLCanvasElement.prototype, "getContext", { value: () => null, writable: true }); + + let interpreter_url = botguard_challenge.interpreter_url ?? ""; + + if (interpreter_url.startsWith("//")) interpreter_url = `https:${interpreter_url}`; + + const bg_script_response = await fetch(interpreter_url); + const interpreter_javascript = await bg_script_response.text(); + + new Function(interpreter_javascript)(); + + const botguard = await BotGuardClient.create({ program: botguard_challenge.program, globalName: botguard_challenge.global_name, globalObject: globalThis }); + + const botguard_response = await botguard.snapshot({ contentBinding: { atr_challenge: binding } }); + return botguard_response; + } +}; + +export async function get_channel_id(yt: Innertube, index?: number): Promise { + const account_info = await yt.account.getInfo(true); + const account = account_info[index ?? 0]; + if (account === undefined) throw new Error("No accounts found"); + const resolve_url = `https://www.youtube.com/${account.channel_handle.text}`; + const resolved = await yt.resolveURL(resolve_url); + const channel_id = resolved.payload.browseId as string; + return channel_id; +} + +export async function get_file_named_buffer_reader(file_path: string): Promise { + const stats = await fs.stat(file_path); + return { + file_name: path.basename(file_path), + source: { + total_bytes: stats.size, + read_chunk: async (position: number, length: number) => { + let handle: fs.FileHandle | undefined = undefined; + try { + handle = await fs.open(file_path, "r"); + const buffer = Buffer.allocUnsafe(length); + const { bytesRead } = await handle.read(buffer, 0, length, position); + return new Uint8Array(buffer.buffer, buffer.byteOffset, bytesRead); + } finally { + await handle?.close(); + } + } + } + }; +} + +export async function get_file_named_buffer_base64(file_path: string): Promise { + return { + file_name: path.basename(file_path), + source: { + base64: (await fs.readFile(file_path, { encoding: "base64" })).toString() + } + }; +} + +export async function youtube_cookies(): Promise { + const HOST_NAME = '.youtube.com'; + const cookies_map = await get_cookies([HOST_NAME]); + const cookies = cookies_map?.[HOST_NAME].toString() ?? undefined; + + if(cookies) console.log(`Successfully found ${cookies_map?.[HOST_NAME].getCookies().length} cookies`); + return cookies; +} \ No newline at end of file diff --git a/examples/youtube-studio-web/tsconfig.json b/examples/youtube-studio-web/tsconfig.json new file mode 100644 index 0000000000..f3edf9b9a4 --- /dev/null +++ b/examples/youtube-studio-web/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "strict": true, + "moduleResolution": "bundler", + "sourceMap": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true, + "allowImportingTsExtensions": true + } +} diff --git a/protos/generated/youtube/api/pfiinnertube/user_info.ts b/protos/generated/youtube/api/pfiinnertube/user_info.ts index 85db5ee6d8..a1583cfe39 100644 --- a/protos/generated/youtube/api/pfiinnertube/user_info.ts +++ b/protos/generated/youtube/api/pfiinnertube/user_info.ts @@ -28,6 +28,19 @@ export interface UserInfo_DelegatePurchases { } export interface UserInfo_DelegationContext { + externalChannelId?: string | undefined; + roleType?: UserInfo_DelegationContext_RoleType | undefined; +} + +export interface UserInfo_DelegationContext_RoleType { + channelRoleType?: UserInfo_DelegationContext_RoleType_ChannelRoleType | undefined; +} + +export enum UserInfo_DelegationContext_RoleType_ChannelRoleType { + CHANNEL_ROLE_TYPE_UNKNOWN = 0, + CREATOR_CHANNEL_ROLE_TYPE_OWNER = 8, + // TODO fill in the rest off the role types + UNRECOGNIZED = -1, } export interface UserInfo_CredentialTransferToken { @@ -221,11 +234,17 @@ export const UserInfo_DelegatePurchases: MessageFns }; function createBaseUserInfo_DelegationContext(): UserInfo_DelegationContext { - return {}; + return { externalChannelId: undefined, roleType: undefined }; } export const UserInfo_DelegationContext: MessageFns = { - encode(_: UserInfo_DelegationContext, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + encode(message: UserInfo_DelegationContext, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.externalChannelId !== undefined) { + writer.uint32(18).string(message.externalChannelId); + } + if (message.roleType !== undefined) { + UserInfo_DelegationContext_RoleType.encode(message.roleType, writer.uint32(42).fork()).join(); + } return writer; }, @@ -236,6 +255,59 @@ export const UserInfo_DelegationContext: MessageFns while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { + case 2: { + if (tag !== 18) { + break; + } + + message.externalChannelId = reader.string(); + continue; + } + case 5: { + if (tag !== 42) { + break; + } + + message.roleType = UserInfo_DelegationContext_RoleType.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, +}; + +function createBaseUserInfo_DelegationContext_RoleType(): UserInfo_DelegationContext_RoleType { + return { channelRoleType: undefined }; +} + +export const UserInfo_DelegationContext_RoleType: MessageFns = { + encode(message: UserInfo_DelegationContext_RoleType, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.channelRoleType !== undefined) { + writer.uint32(8).int32(message.channelRoleType); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): UserInfo_DelegationContext_RoleType { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseUserInfo_DelegationContext_RoleType(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.channelRoleType = reader.int32() as any; + continue; + } } if ((tag & 7) === 4 || tag === 0) { break; diff --git a/protos/youtube/api/pfiinnertube/user_info.proto b/protos/youtube/api/pfiinnertube/user_info.proto index 9ae94541cd..d3a84b71ea 100644 --- a/protos/youtube/api/pfiinnertube/user_info.proto +++ b/protos/youtube/api/pfiinnertube/user_info.proto @@ -14,6 +14,20 @@ message UserInfo { message KidsParent { } message DelegatePurchases { } - message DelegationContext { } + + message DelegationContext { + optional string external_channel_id = 2; + optional RoleType role_type = 5; + + message RoleType { + optional ChannelRoleType channel_role_type = 1; + + enum ChannelRoleType { + CHANNEL_ROLE_TYPE_UNKNOWN = 0; + CREATOR_CHANNEL_ROLE_TYPE_OWNER = 8; + } + } + } + message CredentialTransferToken { } } \ No newline at end of file diff --git a/src/Innertube.ts b/src/Innertube.ts index 208ba486d4..2df3d7fbe8 100644 --- a/src/Innertube.ts +++ b/src/Innertube.ts @@ -1,6 +1,6 @@ import Session from './core/Session.js'; -import { Kids, Music, Studio } from './core/clients/index.js'; +import { Kids, Music, Studio, StudioWeb } from './core/clients/index.js'; import { AccountManager, InteractionManager, PlaylistManager } from './core/managers/index.js'; import { Feed, TabbedFeed } from './core/mixins/index.js'; @@ -143,7 +143,7 @@ export default class Innertube { signatureTimestamp: session.player?.signature_timestamp } }, - client: options?.client + client: options?.client }; if (options?.po_token) { @@ -155,7 +155,7 @@ export default class Innertube { poToken: session.po_token }; } - + const watch_response = await watch_endpoint.call(session.actions, extra_payload); const cpn = generateRandomString(16); @@ -296,7 +296,7 @@ export default class Innertube { 'Cookie': session.cookie || '' } }); - + const text = await response.text(); const data = JSON.parse(text.replace('window.google.ac.h(', '').slice(0, -1)); @@ -560,10 +560,10 @@ export default class Innertube { const payload: Record = { engagementType: engagement_type }; - + if (ids) payload.ids = ids; - + return this.actions.execute('/att/get', { parse: true, ...payload }); } @@ -590,6 +590,14 @@ export default class Innertube { return new Studio(this.#session); } + /** + * An interface for interacting with YouTube Studio Web. + * @param channel_id - the channel id to interface with + */ + studioWeb(channel_id: string) { + return new StudioWeb(this.#session, channel_id); + } + /** * An interface for interacting with YouTube Kids. */ diff --git a/src/core/Actions.ts b/src/core/Actions.ts index ac557fcc88..13b135ec90 100644 --- a/src/core/Actions.ts +++ b/src/core/Actions.ts @@ -1,19 +1,34 @@ import type { IBrowseResponse, + ICreateCaptionsResponse, + ICreateVideoResponse, + IESRChallengeResponse, IGetChallengeResponse, IGetNotificationsMenuResponse, + IGetSessionTokenResponse, + IGetWebReauthURLResponse, + IMetadataUpdateResponse, INextResponse, + IParseCaptionsResponse, IParsedResponse, IPlayerResponse, IRawResponse, IResolveURLResponse, ISearchResponse, - IUpdatedMetadataResponse + IUpdateCaptionsResponse, + IUpdatedMetadataResponse, + IUploadFeedbackResponse } from '../parser/index.js'; import { NavigateAction, Parser } from '../parser/index.js'; -import { InnertubeError } from '../utils/Utils.js'; +import { InnertubeError, u8ToBase64 } from '../utils/Utils.js'; + +import { + UserInfo_DelegationContext, + UserInfo_DelegationContext_RoleType_ChannelRoleType +} from '../../protos/generated/youtube/api/pfiinnertube/user_info.js'; import type { Session } from './index.js'; +import { Constants } from '../utils/index.js'; export interface ApiResponse { success: boolean; @@ -34,14 +49,23 @@ export type InnertubeEndpoint = export type ParsedResponse = T extends '/player' ? IPlayerResponse : - T extends '/search' ? ISearchResponse : - T extends '/browse' ? IBrowseResponse : - T extends '/next' ? INextResponse : - T extends '/updated_metadata' ? IUpdatedMetadataResponse : - T extends '/navigation/resolve_url' ? IResolveURLResponse : - T extends '/notification/get_notification_menu' ? IGetNotificationsMenuResponse : - T extends '/att/get' ? IGetChallengeResponse : - IParsedResponse; + T extends '/search' ? ISearchResponse : + T extends '/browse' ? IBrowseResponse : + T extends '/next' ? INextResponse : + T extends '/updated_metadata' ? IUpdatedMetadataResponse : + T extends '/navigation/resolve_url' ? IResolveURLResponse : + T extends '/notification/get_notification_menu' ? IGetNotificationsMenuResponse : + T extends '/att/get' ? IGetChallengeResponse : + T extends '/att/esr' ? IESRChallengeResponse : + T extends '/ars/grst' ? IGetSessionTokenResponse : + T extends '/security/get_web_reauth_url' ? IGetWebReauthURLResponse : + T extends '/globalization/create_captions' ? ICreateCaptionsResponse : + T extends '/globalization/parse_captions' ? IParseCaptionsResponse : + T extends '/globalization/update_captions' ? IUpdateCaptionsResponse : + T extends '/video_manager/metadata_update' ? IMetadataUpdateResponse : + T extends '/upload/createvideo' ? ICreateVideoResponse : + T extends '/upload/feedback' ? IUploadFeedbackResponse : + IParsedResponse; export default class Actions { public session: Session; @@ -145,6 +169,53 @@ export default class Actions { if (data?.client === 'YTMUSIC') { data.isAudioOnly = true; } + + delete this.session.context.request?.returnLogEntry; + delete this.session.context.request?.eats; + delete this.session.context.request?.reauthRequestInfo; + delete this.session.context.request?.sessionInfo; + delete this.session.context.request?.attestationResponseData; + delete this.session.context.user?.delegationContext; + delete this.session.context.user?.serializedDelegationContext; + if (data?.client === 'WEB_CREATOR') { + if (this.session.context.request) { // should just be true + // TODO maybe I want to manually fetch the initial eats; but it seems that it doesn't matter to much... + if (data?.eats) { + this.session.context.request.eats = data?.eats; + delete data?.eats; + } else { + this.session.context.request.eats = Constants.CLIENTS.WEB_CREATOR.EATS; + } + + if (data.reauth_proof_token) { + this.session.context.request.reauthRequestInfo = { encodedReauthProofToken: data.reauth_proof_token }; + delete data.reauth_proof_token; + } + + if (data.session_token) { + this.session.context.request.sessionInfo = { token: data.session_token }; + delete data.session_token; + } + + if (data.attestation_response_data && Reflect.has(data.attestation_response_data, 'challenge') && Reflect.has(data.attestation_response_data, 'webResponse')) { + this.session.context.request.attestationResponseData = data.attestation_response_data; + } + + if (data.channel_id && this.session.context.user) { + const delegation_context = { + externalChannelId: data.channel_id, + // ?? Not to sure when this is ever not 'CREATOR_CHANNEL_ROLE_TYPE_OWNER', but if it can be else-things then gotta fetch it... + roleType: { channelRoleType: 'CREATOR_CHANNEL_ROLE_TYPE_OWNER' as const } + }; + + this.session.context.user.delegationContext = delegation_context; + this.session.context.user.serializedDelegationContext = u8ToBase64(UserInfo_DelegationContext.encode({ + externalChannelId: delegation_context.externalChannelId, + roleType: { channelRoleType: UserInfo_DelegationContext_RoleType_ChannelRoleType.CREATOR_CHANNEL_ROLE_TYPE_OWNER } + }).finish()); + } + } + } } else if (args) { data = args.serialized_data; } @@ -161,6 +232,17 @@ export default class Actions { } }); + // YouTube Studio Web Context Cleanup + { + delete this.session.context.request?.returnLogEntry; + delete this.session.context.request?.eats; + delete this.session.context.request?.reauthRequestInfo; + delete this.session.context.request?.sessionInfo; + delete this.session.context.request?.attestationResponseData; + delete this.session.context.user?.delegationContext; + delete this.session.context.user?.serializedDelegationContext; + } + if (args?.parse) { let parsed_response = Parser.parseResponse>(await response.json()); diff --git a/src/core/Session.ts b/src/core/Session.ts index 1c1f5c2c20..b171741cc4 100644 --- a/src/core/Session.ts +++ b/src/core/Session.ts @@ -93,6 +93,17 @@ export type Context = { enableSafetyMode: boolean; lockedSafetyMode: boolean; onBehalfOfUser?: string; + // Studio Web params + delegationContext?: { + externalChannelId: string, + roleType: { + channelRoleType: + 'CREATOR_CHANNEL_ROLE_TYPE_OWNER' | 'CREATOR_CHANNEL_ROLE_TYPE_MANAGER' | 'CREATOR_CHANNEL_ROLE_TYPE_EDITOR' | 'CREATOR_CHANNEL_ROLE_TYPE_EDITOR_LIMITED' | + 'CREATOR_CHANNEL_ROLE_TYPE_SUBTITLE_EDITOR' | 'CREATOR_CHANNEL_ROLE_TYPE_VIEWER' | 'CREATOR_CHANNEL_ROLE_TYPE_VIEWER_LIMITED' | 'CREATOR_CHANNEL_ROLE_TYPE_UNSPECIFIED' | + 'CREATOR_CHANNEL_ROLE_TYPE_MODERATOR' | 'CREATOR_CHANNEL_ROLE_TYPE_CUSTOM' + } + } + serializedDelegationContext?: string; }; thirdParty?: { embedUrl: string; @@ -100,6 +111,17 @@ export type Context = { request?: { useSsl: boolean; internalExperimentFlags: any[]; + // Studio Web params + eats?: string; + returnLogEntry?: boolean; + sessionInfo?: { token: string }; + attestationResponseData?: { + challenge: string; + webResponse: string; + }; + reauthRequestInfo?: { + encodedReauthProofToken: string; + }; }; } diff --git a/src/core/clients/StudioWeb.ts b/src/core/clients/StudioWeb.ts new file mode 100644 index 0000000000..12f3b188e7 --- /dev/null +++ b/src/core/clients/StudioWeb.ts @@ -0,0 +1,886 @@ +import type { BotGuardChallenge, BotGuardSolver } from '../../types/BotGuard.js'; +import type { EngagementType } from '../../types/Misc.js'; +import type { FileNamedBufferReader, StudioVisibility, UploadVideoDetails } from '../../types/StudioWebUploading.js'; +import type { ICreateCaptionsResponse, ICreateVideoResponse, IMetadataUpdateResponse, IParseCaptionsResponse, IParsedResponse, IUpdateCaptionsResponse } from '../../parser/index.js'; +import { Constants, Log } from '../../utils/index.js'; +import { InnertubeError, Platform, wait } from '../../utils/Utils.js'; +import type { Actions, ParsedResponse, Session } from '../index.js'; +import { UploadFeedbackItem } from '../../parser/nodes.js'; + +type AttestationPlacement = 'none' | 'context' | 'top_level'; + +type StudioManagedEndpoint = + | '/globalization/create_captions' + | '/globalization/parse_captions' + | '/globalization/update_captions' + | '/video_manager/metadata_update' + | '/upload/createvideo' + | '/upload/feedback'; + +interface BotGuardAttestationResponse { + challenge: string; + webResponse: string; +}; + +interface StudioUnboundChallenge { + bg_challenge: BotGuardChallenge; + challenge: string; + eats: string; + expires_at_ms: number; + result?: string; +}; + +interface StudioBotguardData { + interpreter_url: string; + program: string; +}; + +interface StudioCreatorStudioChallenge { + botguard_data: StudioBotguardData; + challenge: string; + eats: string; +}; + +interface StudioSessionTokenCache { + session_token: string; + expires_at_ms: number; +}; + +interface ScottyStart { upload_url: string; resource_id?: string }; +interface ScottyUploadResult { status?: string; scottyResourceId?: string }; +type ScottyProgress = (written_bytes: number, total_bytes: number) => void; +type ScottyUploadType = 'VIDEO' | 'THUMBNAIL'; + +type UpdateMetadataPayload = Record; + +interface UploadSubtitlesResponse { + created: ICreateCaptionsResponse; + parsed: IParseCaptionsResponse; + updated: IUpdateCaptionsResponse; +} + +interface UploadFeedbackResult { contents: UploadFeedbackItem[], next: () => Promise }; + +const VIDEO_READ_MASK = { + metadataLanguage: { + all: true + }, + notification: { + all: true + }, + status: true, + statusDetails: { + all: true + }, + ownedClaimDetails: { + all: true + }, + thumbnailDetails: { + all: true + }, + videoId: true, + permissions: { + all: true + }, + origin: true, + inlineEditProcessingStatus: true, + monetization: { + all: true + }, + allRestrictions: { + all: true + }, + videoPrechecks: { + all: true + }, + audienceRestriction: { + all: true + }, + mfkSettings: { + all: true + }, + selfCertification: { + all: true + }, + videoStreamUrl: true, + visibility: { + all: true + }, + shorts: { + all: true + }, + responseStatus: { + all: true + }, + contentType: true, + channelId: true, + features: { + all: true + }, + draftStatus: true, + brandDealVideoLink: { + all: true + }, + activeBrandDealVideoLinks: { + all: true + }, + audioLanguage: { + all: true + }, + videoAdvertiserSpecificAgeGates: { + all: true + }, + claimDetails: { + all: true + }, + commentsDisabledInternally: true, + livestream: { + all: true + }, + music: { + all: true + }, + premiere: { + all: true + }, + timePublishedSeconds: true, + uncaptionedReason: true, + remix: { + all: true + }, + contentOwnershipModelSettings: { + all: true + }, + creatorInitiatedVideoChannelLinks: { + all: true + }, + releaseInfo: { + all: true + }, + podcastRssMetadata: { + all: true + }, + googleAdsVideoLinks: { + all: true + }, + alteredContentSettings: { + all: true + }, + collaboration: { + all: true + }, + videoResolutions: { + all: true + }, + publicLivestream: { + all: true + }, + publicPremiere: { + all: true + }, + thumbnailEditorState: { + all: true + }, + title: true, + videoCreatorExperiment: { + all: true + }, + lengthSeconds: true, + tvfilmMetadata: { + all: true + }, + privacy: true, + shareUrl: true, + scheduledPublishingDetails: { + all: true + }, + privateShare: { + all: true + }, + sponsorsOnly: { + all: true + }, + superfansOnly: { + all: true + }, + unlistedExpired: true, + videoTrailers: { + all: true + }, + isPaygated: true, + suggestions: { + all: true + }, + tvType: { + all: true + }, + genres: { + all: true + }, + episode: { + all: true + }, + titleDetails: { + all: true + }, + copyrightSummary: { + all: true + }, + productSelection: { + all: true + }, + productAutotaggingSettings: { + all: true + }, + videoLinkageShortsAttribution: { + all: true + }, + academicLearning: { + all: true + }, + allowEmbed: true, + allowRatings: true, + ageRestriction: true, + category: true, + commentFilter: true, + commentSettings: { + all: true + }, + crowdsourcingEnabled: true, + dateRecorded: { + all: true + }, + defaultCommentSortOrder: true, + description: true, + descriptionFormattedString: { + all: true + }, + gameTitle: { + all: true + }, + license: true, + liveChat: { + all: true + }, + location: { + all: true + }, + paidProductPlacement: true, + paidPoliticalContent: { + all: true + }, + publishing: { + all: true + }, + tags: { + all: true + }, + titleFormattedString: { + all: true + }, + videoDurationMs: true, + viewCountIsHidden: true, + autoChapterSettings: { + all: true + }, + autoPlacesMentionedSettings: { + all: true + }, + videoArtworkEditorState: { + all: true + }, + learningConceptSettings: { + all: true + }, + videoEditorProject: { + videoDimensions: { + all: true + } + }, + originalFilename: true, + timeCreatedSeconds: true, + files: { + all: true + }, + adSettings: { + all: true + }, + monetizedStatus: true, + serializedShareEntity: true, + publicMetrics: { + all: true + } +} as const; + +const CREATOR_VIDEO_CATEGORY_IDS = { + FILM: 1, AUTOS: 2, MUSIC: 10, PETS: 15, SPORTS: 17, TRAVEL: 19, GADGETS: 20, + PEOPLE: 22, COMEDY: 23, ENTERTAINMENT: 24, NEWS: 25, HOWTO: 26, EDUCATION: 27, + SCIENCE: 28, GOVERNMENT: 29 +} as const; +const ALLOW_COMMENT_MODES = { + NONE: 'ALL_COMMENTS', + BASIC: 'AUTOMATED_COMMENTS', + STRICT: 'AUTO_MODERATED_COMMENTS_HOLD_MORE', + HOLD_ALL: 'APPROVED_COMMENTS' +} as const; +const COMMENT_ENABLED_STATES = { + ON: 'MDE_COMMENT_ENABLED_STATE_ON', + OFF: 'MDE_COMMENT_ENABLED_STATE_OFF', + PAUSE: 'MDE_COMMENT_ENABLED_STATE_PAUSED' +} as const; +const ALLOWED_COMMENTER_MODES = { + ANYONE: 'MDE_ALLOWED_COMMENTER_MODE_ANYONE', + SUBSCRIBERS_AND_MEMBERS: 'MDE_ALLOWED_COMMENTER_MODE_SUBSCRIBERS_MEMBERS_APPROVED_USERS' +} as const; +const COMMENT_SORT_ORDERS = { + TOP: 'MDE_COMMENT_SORT_ORDER_TOP', + NEWEST: 'MDE_COMMENT_SORT_ORDER_LATEST' +} as const; +const REMIX_SOURCE_OPTIONS = { + ALLOW_VIDEO_AND_AUDIO: 'MDE_REMIX_SOURCE_OPTION_OPT_IN', + ALLOW_ONLY_AUDIO: 'MDE_REMIX_SOURCE_OPTION_VISUAL_OPT_OUT_AND_PERFORM_ACTIONS', + DONT_ALLOW: 'MDE_REMIX_SOURCE_OPTION_OPT_OUT_AND_MUTE_DERIVATIVES' +} as const; + +const UPLOAD_TYPES_TO_START_URL: Record = { + VIDEO: Constants.URLS.YT_UPLOAD_VIDEO_WEB, + THUMBNAIL: Constants.URLS.YT_UPLOAD_THUMBNAIL_WEB +} as const; + +export default class StudioWeb { + #session: Session; + #actions: Actions; + #channel_id: string; + #botguard_solver: BotGuardSolver|null; + #unbound_challenge_cache: StudioUnboundChallenge|undefined; + #auto_retry: boolean; + #force_refresh_session_token: boolean; + #channel_id_session_token_cache: StudioSessionTokenCache | null; + + constructor(session: Session, channel_id: string) { + this.#session = session; + this.#actions = session.actions; + this.#channel_id = channel_id; + this.#botguard_solver = null; + this.#auto_retry = true; + this.#force_refresh_session_token = false; + this.#channel_id_session_token_cache = null; + if (!session.logged_in) + throw new InnertubeError('You must be signed in to use this client.'); + } + + setBotGuardSolver(botguard_solver: BotGuardSolver) { + this.#botguard_solver = botguard_solver; + } + setAutoRetrying(auto_retry: boolean) { + this.#auto_retry = auto_retry; + } + + async #attGet(engagement_type: EngagementType, ids?: Record[], eats?: string) { + const payload: Record = { + engagementType: engagement_type + }; + + if (ids) payload.ids = ids; + + return this.#actions.execute('/att/get', { client: 'WEB_CREATOR', parse: true, ...payload, ...(eats ? { eats } : {}) }); + } + + #challengeExpiryAtMs(challenge?: string): number { + if (challenge === undefined) return Date.now(); + const params = new URLSearchParams(challenge); + const issued_seconds = Number(params.get('c')); + const ttl_seconds = Number(params.get('t')); + if (isNaN(issued_seconds) || isNaN(ttl_seconds) || issued_seconds === 0) return Date.now(); + return (issued_seconds + ttl_seconds) * 1000; + } + + async #getUnboundChallenge(): Promise { + if (this.#unbound_challenge_cache !== undefined && this.#unbound_challenge_cache.expires_at_ms > Date.now()) return this.#unbound_challenge_cache; + const unbound_challenge = await this.#attGet('ENGAGEMENT_TYPE_UNBOUND'); + + if (!unbound_challenge.eats) throw new InnertubeError('Unbound challenge missing "eats"'); + if (!unbound_challenge.challenge) throw new InnertubeError('Unbound challenge missing "challenge"'); + if (!unbound_challenge.bg_challenge) throw new InnertubeError('Unbound challenge missing "bg_challenge"'); + + this.#unbound_challenge_cache = { + bg_challenge: { + ...unbound_challenge.bg_challenge, + interpreter_url: unbound_challenge.bg_challenge.interpreter_url.private_do_not_access_or_else_safe_script_wrapped_value ?? unbound_challenge.bg_challenge.interpreter_url.private_do_not_access_or_else_trusted_resource_url_wrapped_value ?? '' + }, + challenge: unbound_challenge.challenge, + eats: unbound_challenge.eats, + expires_at_ms: this.#challengeExpiryAtMs(unbound_challenge.challenge) + }; + if (!this.#unbound_challenge_cache.bg_challenge.interpreter_url) throw new InnertubeError('Unbound challenge bg_challenge missing valid "interpreter_url"'); + return this.#unbound_challenge_cache; + } + + async #getCreatorStudioChallenge(unbound_challenge_eats: string): Promise { + const creator_studio_challenge = await this.#attGet('ENGAGEMENT_TYPE_CREATOR_STUDIO_ACTION', [ + { externalChannelId: this.#channel_id } + ], unbound_challenge_eats); + + if (!creator_studio_challenge.eats) throw new InnertubeError('Creator Studio challenge missing "eats"'); + if (!creator_studio_challenge.challenge) throw new InnertubeError('Creator Studio challenge missing "challenge"'); + if (!creator_studio_challenge.botguard_data) throw new InnertubeError('Creator Studio challenge missing "botguard_data"'); + + const interpreter_url = creator_studio_challenge.botguard_data.interpreter_url.private_do_not_access_or_else_safe_script_wrapped_value ?? creator_studio_challenge.botguard_data.interpreter_url.private_do_not_access_or_else_trusted_resource_url_wrapped_value ?? ''; + if (!interpreter_url) throw new InnertubeError('Creator Studio challenge bg_challenge missing valid "interpreter_url"'); + return { + botguard_data: { + ...creator_studio_challenge.botguard_data, + interpreter_url: interpreter_url + }, + challenge: creator_studio_challenge.challenge, + eats: creator_studio_challenge.eats + }; + } + + async #getBotGuardAttestation(): Promise { + if (!this.#botguard_solver) throw new InnertubeError('BotGuard Solver is not initialized. Please setup with setBotGuardSolver()'); + const unbound_challenge = this.#unbound_challenge_cache && this.#unbound_challenge_cache.expires_at_ms > Date.now() ? this.#unbound_challenge_cache : await this.#getUnboundChallenge(); + if (unbound_challenge.result) return { challenge: unbound_challenge.challenge, webResponse: unbound_challenge.result }; + const botguard_response = await this.#botguard_solver.solve(unbound_challenge.bg_challenge, unbound_challenge.challenge); + unbound_challenge.result = botguard_response; + this.#unbound_challenge_cache = unbound_challenge; + return { challenge: unbound_challenge.challenge, webResponse: botguard_response }; + } + + #eatForceRefreshSessionToken() { + const force_refresh_session_token = this.#force_refresh_session_token; + this.#force_refresh_session_token = false; + return force_refresh_session_token; + } + + async getSessionToken(): Promise { + if (!this.#botguard_solver) throw new InnertubeError('BotGuard Solver is not initialized. Please setup with setBotGuardSolver()'); + if (this.#channel_id_session_token_cache && this.#channel_id_session_token_cache.expires_at_ms > Date.now() && !this.#eatForceRefreshSessionToken()) + return this.#channel_id_session_token_cache.session_token; + + const unbound_challenge = await this.#getUnboundChallenge(); + const creator_studio_challenge = await this.#getCreatorStudioChallenge(unbound_challenge.eats); + + // don't cache this result into the unbounded cache since it uses different challenge kinda + const botguard_response = await this.#botguard_solver.solve(unbound_challenge.bg_challenge, creator_studio_challenge.challenge); + + const esr_data = await this.#actions.execute('/att/esr', { + client: 'WEB_CREATOR', + parse: true, + challenge: creator_studio_challenge.challenge, + botguardResponse: botguard_response, + xguardClientStatus: 0, + eats: creator_studio_challenge.eats + }); + + if (!esr_data.ctx || esr_data.should_fetch_reauth_session_token === undefined) throw new InnertubeError('/att/esr did not return usable data'); + + let grst_ctx = esr_data.ctx; + let reauth_proof_token: string | undefined; + if (esr_data.should_fetch_reauth_session_token === true) { + const reauth_data = await this.#actions.execute('/security/get_web_reauth_url', { + client: 'WEB_CREATOR', + parse: true, + continueUrl: `${Constants.URLS.YT_STUDIO_WEB_BASE}/reauth`, + flow: 'REAUTH_FLOW_YT_STUDIO_COLD_LOAD', + ivctx: esr_data.ctx, + challenge: creator_studio_challenge.challenge, + botguardResponse: botguard_response, + eats: creator_studio_challenge.eats + }); + if (!reauth_data.encoded_reauth_proof_token || !reauth_data.session_risk_ctx) throw new InnertubeError('/security/get_web_reauth_url did not return a reauth proof'); + grst_ctx = reauth_data.session_risk_ctx; + reauth_proof_token = reauth_data.encoded_reauth_proof_token; + } + + const grst_data = await this.#actions.execute('/ars/grst', { client: 'WEB_CREATOR', parse: true, ctx: grst_ctx, reauth_proof_token, eats: creator_studio_challenge.eats }); + + if (grst_data.session_token === undefined) throw new InnertubeError('/ars/grst did not return a session token'); + this.#channel_id_session_token_cache = { + session_token: grst_data.session_token, + expires_at_ms: unbound_challenge.expires_at_ms + }; + return grst_data.session_token; + } + + #scottyHeaders(file_name: string): Record { + if (!this.#session.cookie) throw new InnertubeError('Unable to produce scottyHeaders with cookies'); + return { + 'Cookie': this.#session.cookie, + 'Accept': '*/*', + 'Accept-Language': 'en-US,en;q=0.9', + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + 'Origin': Constants.URLS.YT_STUDIO_WEB_BASE, + 'Referer': `${Constants.URLS.YT_STUDIO_WEB_BASE}/`, + 'User-Agent': Constants.CLIENTS['WEB_CREATOR'].USER_AGENT, + 'x-goog-upload-file-name': encodeURIComponent(file_name) + }; + } + + async #scottyStart(start_upload_url: string, file_name_buffer_reader: FileNamedBufferReader, start_payload: object): Promise { + if (file_name_buffer_reader.source.total_bytes <= 0) throw new InnertubeError('Can\'t upload an empty file to scotty'); + + const start_response = await fetch(`${start_upload_url}?authuser=0`, { + method: 'POST', + headers: { + ...this.#scottyHeaders(file_name_buffer_reader.file_name), + 'x-goog-upload-command': 'start', + 'x-goog-upload-header-content-length': String(file_name_buffer_reader.source.total_bytes), + 'x-goog-upload-protocol': 'resumable' + }, + referrer: Constants.URLS.YT_STUDIO_WEB_BASE, + body: JSON.stringify(start_payload) + }); + const upload_url = start_response.headers.get('x-goog-upload-url'); + if (upload_url === null || upload_url === '') throw new InnertubeError('Scotty did not return an upload url'); + + const resource_id = start_response.headers.get('x-goog-upload-header-scotty-resource-id'); + + return { upload_url, resource_id: !resource_id ? undefined : resource_id }; + } + + async #scottyUploadChunks(upload_url: string, file_name_buffer_reader: FileNamedBufferReader, on_progress?: ScottyProgress): Promise<{ resource_id?: string }> { + const UPLOAD_CHUNK_SIZE_BYTES = 100 * 1024 * 1024; + + let offset = 0; + while (offset < file_name_buffer_reader.source.total_bytes) { + const chunk = await file_name_buffer_reader.source.read_chunk(offset, Math.min(UPLOAD_CHUNK_SIZE_BYTES, file_name_buffer_reader.source.total_bytes - offset)); + if (chunk.byteLength === 0) throw new InnertubeError('Ran out of bytes before scotty finalized the upload'); + + const is_final = offset + chunk.byteLength >= file_name_buffer_reader.source.total_bytes; + const chunk_response = await fetch(upload_url, { + method: 'POST', + headers: { + ...this.#scottyHeaders(file_name_buffer_reader.file_name), + 'x-goog-upload-command': is_final ? 'upload, finalize' : 'upload', + 'x-goog-upload-offset': String(offset) + }, + body: chunk as BodyInit + }); + if (!chunk_response.ok) throw new InnertubeError('Unable to upload a chunk of the buffer to scotty'); + + offset += chunk.byteLength; + on_progress?.(offset, file_name_buffer_reader.source.total_bytes); + if (!is_final) { + await chunk_response.body?.cancel(); + continue; + } + + const result = await chunk_response.json() as ScottyUploadResult; + if (result.status !== 'STATUS_SUCCESS') throw new InnertubeError('Scotty did not finalize the upload'); + return { resource_id: result.scottyResourceId }; + } + throw new InnertubeError('Scotty upload loop ended without finalizing'); + } + + async #uploadToScotty(upload_type: ScottyUploadType, file_name_buffer_reader: FileNamedBufferReader, start_payload: object, on_progress?: ScottyProgress): Promise { + const start = await this.#scottyStart(UPLOAD_TYPES_TO_START_URL[upload_type], file_name_buffer_reader, start_payload); + const uploaded = await this.#scottyUploadChunks(start.upload_url, file_name_buffer_reader, on_progress); + const resource_id = start.resource_id ?? uploaded.resource_id; + if (!resource_id) throw new InnertubeError('Scotty did not return a resource id'); + return resource_id; + } + + async #uploadThumbnailResource(file_name_buffer_reader: FileNamedBufferReader): Promise { + return await this.#uploadToScotty('THUMBNAIL', file_name_buffer_reader, {}); + } + + async managedExecute(endpoint: T, payload: object, channel_id?: string, attestation_placement: AttestationPlacement = 'none', eats?: string, is_retry = false): Promise> { + const attestation_response_data = attestation_placement === 'none' ? undefined : await this.#getBotGuardAttestation(); + + const data = await this.#actions.execute(endpoint, { + client: 'WEB_CREATOR', + parse: true, + session_token: await this.getSessionToken(), + ...payload, + ...(attestation_placement === 'context' ? { attestation_response_data } : {}), + ...(attestation_placement === 'top_level' ? { attestationResponseData: attestation_response_data } : {}), + ...(!eats ? {} : { eats }), + ...(!channel_id ? {} : { channel_id }) + }) as IParsedResponse; + + if (data.challenge_prompt_type === 'CHALLENGE_PROMPT_TYPE_AUTHENTICATE') { + if (!is_retry && this.#auto_retry && channel_id !== undefined) { + this.#force_refresh_session_token = true; + return await this.managedExecute(endpoint, payload, channel_id, attestation_placement, eats, true); + } + throw new InnertubeError('YouTube Studio is requesting an authentication challenge, likely a stale session token'); + } + return data as ParsedResponse; + } + + async uploadSubtitles(video_id: string, subtitles: NonNullable, language = 'en-US'): Promise { + const data_base64 = await subtitles.data.source.base64; + const data_uri = `data:application/octet-stream;base64,${data_base64}`; + const tts_track_id = { lang: language, kind: '', name: '' }; + + const created = await this.managedExecute('/globalization/create_captions', { + videoId: video_id, + channelId: this.#channel_id, + newTrack: tts_track_id, + overwrite: subtitles.overwrite ?? true, + autoTranslate: subtitles.auto_translate ?? false + }, this.#channel_id); + + const content_update_time = created.translation?.captions_translations?.[0]?.content_update_time; + if (content_update_time === undefined) throw new InnertubeError('create_captions did not return a contentUpdateTime'); + + const parsed = await this.managedExecute('/globalization/parse_captions', { + fileType: subtitles.synced ? 'CAPTIONS_FILE_TYPE_TIMED_TEXT' : 'CAPTIONS_FILE_TYPE_TRANSCRIPT', + fileName: subtitles.data.file_name, + dataUri: data_uri + }, this.#channel_id); + + const updated = await this.managedExecute('/globalization/update_captions', { + videoId: video_id, + channelId: this.#channel_id, + operations: [ + { + ttsTrackId: tts_track_id, + userIntent: 'USER_INTENT_EDIT_LATEST_DRAFT', + vote: 'VOTE_PUBLISH', + isContentEdited: false, + contentUpdateTime: content_update_time, + captionsFile: { dataUri: data_uri, fileName: subtitles.data.file_name } + } + ] + }, this.#channel_id); + return { created, parsed, updated }; + } + #isNumberString(value: string): boolean { + return !!value && !isNaN(Number(value)); + } + + #buildCommentOptions(details: Partial): UpdateMetadataPayload | undefined { + const enabled_state = details.allow_comments === undefined ? undefined : COMMENT_ENABLED_STATES[details.allow_comments]; + const has_options = enabled_state !== undefined + || details.sort_comments_by !== undefined + || details.show_how_many_viewers_like_this_video !== undefined + || details.comment_moderation !== undefined + || details.who_can_comment !== undefined; + if (!has_options) return undefined; + + const comment_options: UpdateMetadataPayload = {}; + if (enabled_state !== undefined) comment_options.newCommentEnabledState = enabled_state; + if (details.sort_comments_by !== undefined) comment_options.newDefaultSortOrder = COMMENT_SORT_ORDERS[details.sort_comments_by]; + if (details.show_how_many_viewers_like_this_video !== undefined) comment_options.newCanViewRatings = details.show_how_many_viewers_like_this_video; + if (enabled_state === undefined || enabled_state === COMMENT_ENABLED_STATES.ON) { + if (details.comment_moderation !== undefined) comment_options.newAllowCommentsMode = ALLOW_COMMENT_MODES[details.comment_moderation]; + if (details.who_can_comment !== undefined) comment_options.newAllowedCommenterMode = ALLOWED_COMMENTER_MODES[details.who_can_comment]; + } + return comment_options; + } + + #buildMetadataUpdate(details: UploadVideoDetails, thumbnail_resource_id?: string): UpdateMetadataPayload { + const payload: UpdateMetadataPayload = {}; + + if (details.title !== undefined) payload.title = { newTitle: details.title, titleOperation: 'MDE_TEXT_UPDATE_OPERATION_SET' }; + if (details.description !== undefined) payload.description = { newDescription: details.description, descriptionOperation: 'MDE_TEXT_UPDATE_OPERATION_SET' }; + if (details.tags !== undefined) payload.tags = { newTags: details.tags }; + if (details.playlists !== undefined) payload.addToPlaylist = { addToPlaylistIds: details.playlists, deleteFromPlaylistIds: [] }; + if (details.audience !== undefined) { + payload.madeForKids = { + operation: 'MDE_MADE_FOR_KIDS_UPDATE_OPERATION_SET', + newMfk: details.audience === 'MADE_FOR_KIDS' ? 'MDE_MADE_FOR_KIDS_TYPE_MFK' : 'MDE_MADE_FOR_KIDS_TYPE_NOT_MFK' + }; + } + if (details.paid_promotion !== undefined) payload.productPlacement = { newHasPaidProductPlacement: details.paid_promotion }; + if (details.ai_use !== undefined) { + payload.alteredContent = { + operation: 'MDE_ALTERED_CONTENT_UPDATE_OPERATION_SET', + newCreatorDisclosedHasAlteredContent: details.ai_use ? 'MDE_HAS_ALTERED_CONTENT_YES' : 'MDE_HAS_ALTERED_CONTENT_NO' + }; + } + + if (details.collaboration_channels) { + payload.collaboration = { + inviteCollaborators: details.collaboration_channels.map((channel) => ({ externalChannelId: channel.id, analyticsSetting: channel.analytics_setting })) + }; + } + + if (details.automatic_chapters !== undefined) payload.autoChapter = { creatorOptOut: !details.automatic_chapters }; + if (details.featured_places !== undefined) payload.autoPlaces = { creatorOptOut: !details.featured_places }; + if (details.automatic_concepts !== undefined) payload.learningConcepts = { autoConceptsCreatorOptOut: !details.automatic_concepts }; + + if (details.video_language !== undefined) payload.audioLanguage = { newAudioLanguage: details.video_language }; + if (details.title_and_description_language !== undefined) payload.metadataLanguage = { newMetadataLanguage: details.title_and_description_language }; + if (details.caption_certification !== undefined) payload.captionsCertificate = { newUncaptionedReason: details.caption_certification }; + + if (details.recording_date !== undefined) { + payload.recordedDate = { + operation: 'MDE_RECORDED_DATE_UPDATE_OPERATION_SET', + newRecordedDate: { + year: details.recording_date.getFullYear(), + month: details.recording_date.getMonth() + 1, + day: details.recording_date.getDate() + } + }; + } + if (details.video_location !== undefined) { + payload.location = { operation: 'MDE_LOCATION_UPDATE_OPERATION_SET_LOCATION', description: details.video_location }; + } + + if (details.license !== undefined) payload.license = { newLicenseId: details.license }; + if (details.allow_embedding !== undefined) payload.distributionOptions = { newAllowEmbedding: details.allow_embedding }; + if (details.publish_to_subscriptions_feed_and_notify_subscribers !== undefined) { + payload.publishingOptions = { newPostToFeed: details.publish_to_subscriptions_feed_and_notify_subscribers }; + } + if (details.shorts_remixing !== undefined) { + payload.remix = { operation: 'MDE_REMIX_UPDATE_OPERATION_SET', newRemixSourceOption: REMIX_SOURCE_OPTIONS[details.shorts_remixing] }; + } + if (details.category !== undefined) { + const category_id = this.#isNumberString(details.category) ? Number(details.category) : CREATOR_VIDEO_CATEGORY_IDS[details.category.toUpperCase() as keyof typeof CREATOR_VIDEO_CATEGORY_IDS]; + if (category_id !== undefined) payload.category = { newCategoryId: category_id }; + } + + if (details.visibility) { + payload.privacyState = { newPrivacy: details.visibility }; + } + + const comment_options = this.#buildCommentOptions(details); + if (comment_options !== undefined) payload.commentOptions = comment_options; + + if (thumbnail_resource_id !== undefined) { + payload.videoStill = { + operation: 'UPLOAD_CUSTOM_THUMBNAIL', + image: { + encryptedScottyResourceId: thumbnail_resource_id, + name: 'CUSTOM_THUMBNAIL_IMAGE_NAME_DEFAULT', + format: 'CUSTOM_THUMBNAIL_IMAGE_FORMAT_JPEG' + } + }; + } + + return payload; + } + + async #updateMetadata(video_id: string, payload: UpdateMetadataPayload): Promise { + return await this.managedExecute('/video_manager/metadata_update', { + encryptedVideoId: video_id, + videoReadMask: VIDEO_READ_MASK, + flowType: 'MDE_FLOW_TYPE_UPLOAD', + ...payload + }, this.#channel_id, 'top_level'); + } + + async updateVideo(video_id: string, details: Partial) { + let thumbnail_resource_id: string | undefined; + if (details.thumbnail !== undefined) { + const resource_id = await this.#uploadThumbnailResource(details.thumbnail); + thumbnail_resource_id = resource_id; + } + + const payload = this.#buildMetadataUpdate(details, thumbnail_resource_id); + + let update_metadata_response: IMetadataUpdateResponse | null = null; + let update_subtitles_response: UploadSubtitlesResponse | null = null; + + if (Object.keys(payload).length > 0) { + update_metadata_response = await this.#updateMetadata(video_id, payload); + } + if (details.subtitles !== undefined) { + update_subtitles_response = await this.uploadSubtitles(video_id, details.subtitles, details.video_language); + } + return { update_metadata_response, update_subtitles_response }; + } + + async publishVideo(video_id: string, visibility: StudioVisibility = 'PRIVATE') { + const update_metadata_response = await this.#updateMetadata(video_id, { + privacyState: { newPrivacy: visibility }, + draftState: { operation: 'MDE_DRAFT_STATE_UPDATE_OPERATION_REMOVE_DRAFT_STATE' } + }); + return update_metadata_response; + } + + // from https://studio.youtube.com/youtubei/v1/creator/get_channel_dashboard?alt=json under interactionRecordingParams; likely useless + async uploadFeedback(tokens: string[], type: 'FEEDBACK_TOKENS'): Promise<{ isProcessed: boolean }> + // initial from createvideo under uploadFeedbackRefreshContinuation + async uploadFeedback(tokens: string[], type: 'CONTINUATION_TOKENS'): Promise + async uploadFeedback(tokens: string[], type: 'FEEDBACK_TOKENS' | 'CONTINUATION_TOKENS'): Promise<{ isProcessed: boolean } | UploadFeedbackResult | null> { + if (tokens.filter((token) => token).length === 0) return null; + const feedback_data = await this.managedExecute('/upload/feedback', + type === 'CONTINUATION_TOKENS' ? { continuations: tokens } : { feedbackTokens: tokens }); + try { + if (type === 'FEEDBACK_TOKENS') return feedback_data.feedback_responses?.[0] as { isProcessed: boolean }; + const contents = (feedback_data.continuation_contents_array ?? []) as UploadFeedbackItem[]; + return { + contents, next: async () => await this.uploadFeedback([ + contents[0]?.as(UploadFeedbackItem)?.continuation_token ?? '' + ], 'CONTINUATION_TOKENS') + }; + } catch (_) { + return null; + } + } + + async uploadFeedbackCycle(initial_tokens: (string | null)[], callback_continue: (content: UploadFeedbackItem[]) => boolean) { + if (initial_tokens.some((token) => token === null)) return; + let feedback: null | UploadFeedbackResult = await this.uploadFeedback(initial_tokens as string[], 'CONTINUATION_TOKENS'); + if (feedback === null) return; + do { + try { + if (!callback_continue(feedback.contents)) break; + const delay = feedback.contents[0]?.continuation_delay_ms ?? null; + if (delay === null) { + Log.warn('upload feedback delay is null; exiting for safety'); + break; + } + if (delay < 1000) { + Log.warn('upload feedback delay is unusually low; exiting for safety'); + break; + } + await wait(delay); + } catch (e) { + const error = e as Error; + Log.warn(error.message); + } + } + while ((feedback = await feedback.next()) !== null); + } + + async uploadVideo(file: FileNamedBufferReader, details: Partial = {}, on_scotty_progress?: (written_bytes: number, total_bytes: number) => void, + on_initial_create_video?: (full_created: { created: ICreateVideoResponse, feedback_token: string | null }) => any) { + const frontend_upload_id = `innertube_studio:${Platform.shim.uuidv4().toUpperCase()}:0`; + const start = await this.#scottyStart(UPLOAD_TYPES_TO_START_URL['VIDEO'], file, { frontendUploadId: frontend_upload_id }); + + const chunks_uploaded = this.#scottyUploadChunks(start.upload_url, file, on_scotty_progress); + + if (!start.resource_id) throw new InnertubeError('Scotty didn\'t resolve a resource ID'); + + const created = await this.managedExecute('/upload/createvideo', { + channelId: this.#channel_id, + resourceId: { scottyResourceId: { id: start.resource_id } }, + frontendUploadId: frontend_upload_id, + initialMetadata: { + title: { newTitle: details.title ?? file.file_name }, + privacy: { newPrivacy: 'PRIVATE' }, + draftState: { isDraft: true }, + ...(details.tags === undefined ? {} : { tags: { newTags: details.tags } }), + targetedAudience: { + operation: 'MDE_TARGETED_AUDIENCE_UPDATE_OPERATION_SET', + newTargetedAudience: 'MDE_TARGETED_AUDIENCE_TYPE_ALL' + } + }, + contentLevelProtection: { enableRequiresContentLevelProtection: false }, + presumedShort: false + }, this.#channel_id, 'context'); + + on_initial_create_video?.({ created, feedback_token: created.contents?.item()?.as(UploadFeedbackItem).continuation_token ?? null }); + + const video_id = created.video_id; + if (video_id === undefined || video_id === '') { + await chunks_uploaded; + throw new InnertubeError('createvideo did not return a videoId'); + } + + await chunks_uploaded; + + // title and tags already went up with createvideo + const { title: _title, tags: _tags, visibility, ...remaining_details } = details; + const updated = await this.updateVideo(video_id, remaining_details); + + const published = await this.publishVideo(video_id, visibility); + + return { created, updated, published }; + } +} diff --git a/src/core/clients/index.ts b/src/core/clients/index.ts index 6b7fe70544..b05aed26e7 100644 --- a/src/core/clients/index.ts +++ b/src/core/clients/index.ts @@ -1,3 +1,4 @@ export { default as Kids } from './Kids.js'; export { default as Music } from './Music.js'; -export { default as Studio } from './Studio.js'; \ No newline at end of file +export { default as Studio } from './Studio.js'; +export { default as StudioWeb } from './StudioWeb.js'; \ No newline at end of file diff --git a/src/parser/classes/ytstudio/CreatorVideo.ts b/src/parser/classes/ytstudio/CreatorVideo.ts new file mode 100644 index 0000000000..c5ef7c835e --- /dev/null +++ b/src/parser/classes/ytstudio/CreatorVideo.ts @@ -0,0 +1,918 @@ +import { YTNode } from '../../helpers.js'; +import { type RawNode } from '../../index.js'; +import Thumbnail from '../misc/Thumbnail.js'; +import VideoUploadChecks from './VideoUploadChecks.js'; + +export type CreatorVideoStatus = 'DELETED' | 'FAILED' | 'PROCESSED' | 'REJECTED' | 'UNKNOWN' | 'UPLOADED' | 'WRONG'; + +export type DraftStatus = 'NONE' | 'PUBLIC' | 'SCHEDULED'; + +export type CreatorVideoPrivacy = 'PRIVATE' | 'PUBLIC' | 'SCHEDULED' | 'UNKNOWN' | 'UNLISTED'; + +export type CreatorVideoCategory = + 'AUTOS' | 'COMEDY' | 'EDUCATION' | + 'ENTERTAINMENT' | 'FILM' | 'GADGETS' | + 'GOVERNMENT' | 'HOWTO' | 'MUSIC' | + 'NEWS' | 'PEOPLE' | 'PETS' | + 'SCIENCE' | 'SPORTS' | 'TRAVEL' | + 'UNKNOWN'; + +export type VideoCommentFilter = + 'APPROVE' | 'AUTO_MODERATE' | + 'AUTO_MODERATE_HOLD_MORE' | 'NONE' | 'UNKNOWN'; + +export type VideoCommentSortOrder = 'LATEST' | 'TOP'; + +export type VideoAgeRestriction = 'APPEAL_BUTTON' | 'NONE' | 'SELF' | 'SYSTEM'; + +export type VideoLicense = 'CREATIVE_COMMONS' | 'STANDARD' | 'UNKNOWN'; + +export type VideoUncaptionedReason = + 'CAPTIONLESS_TV_CONTENT' | 'EXCEPTION_GRANTED' | + 'LEGACY' | 'NO_FULL_LENGTH_VIDEO' | + 'NO_US_TV_CONTENT' | 'NOT_REQUIRED' | + 'UNNECESSARY_OR_NOT_SET'; + +export type VideoSubscriberNotification = + 'DISABLED' | 'ENABLED' | 'NONE' | 'UNKNOWN'; + +export type VideoPaidProductPlacement = + 'NO' | 'NOTIFY' | 'UNKNOWN' | 'UNSET' | 'YES'; + +export type VideoMusicLicensedStatus = + 'CLAIMED_BY_CHANNEL_OWNER' | 'DISABLED_BY_CLAIM_PREFERENCE' | + 'ENABLED_BY_CHANNEL_OWNER' | 'ENABLED_BY_CHANNEL_OWNER_UNCLAIMED' | + 'ENABLED_BY_PARTNER_UPLOADED_CLAIM' | 'ENABLED_BY_THIRD_PARTY_CLAIMS' | + 'UNCLAIMED'; + +export type VideoUserSetMonetization = 'OFF' | 'ON'; + +export type VideoMonetizingStatus = + 'INDETERMINATE' | 'MONETIZING' | + 'MONETIZING_CREATOR_REVSHARE' | 'MONETIZING_IN_ESCROW' | + 'MONETIZING_WITH_EXCEPTIONS' | 'MONETIZING_WITH_LIMITED_ADS' | + 'MONETIZING_WITH_REVSHARE' | 'NOT_FOR_DISPLAY' | + 'NOT_MONETIZING_CHANNEL_NOT_MONETIZING' | 'NOT_MONETIZING_INELIGIBLE' | + 'NOT_MONETIZING_INELIGIBLE_INNOCUOUS' | 'NOT_MONETIZING_OFF' | + 'NOT_MONETIZING_OFF_CREATOR_REVSHARE' | 'NOT_MONETIZING_OFF_SHORTS_REVSHARE' | + 'NOT_MONETIZING_OFF_WITH_REVSHARE' | 'PENDING_CHECKS' | + 'UNSPECIFIED' | 'VIDEO_NOT_FINAL'; + +export type VideoVisibilityStatus = + 'AGE_RESTRICTED' | 'BLOCKED_FOR_COPYRIGHT_GLOBALLY' | + 'BLOCKED_FOR_COPYRIGHT_PARTIALLY' | 'DRAFT' | + 'FORCED_PRIVATE' | 'INDETERMINATE' | + 'LIMITED_FEATURES' | 'PENDING_REMOVAL_FOR_COPYRIGHT' | + 'REMOVED_FOR_COMMUNITY_GUIDELINES' | 'REMOVED_FOR_COPYRIGHT' | + 'TARGETED_FOR_KIDS' | 'UNKNOWN' | + 'UPLOAD_FAILED' | 'UPLOADING_OR_PROCESSING' | + 'USER_CONFIG'; + +export type VideoUserSetVisibility = + 'DRAFT' | 'FUTURE_PREMIERE' | 'MEMBERS' | + 'PREMIERING' | 'PRIMETIME_SUBSCRIBERS' | 'PRIVATE' | + 'PUBLIC' | 'SCHEDULED' | + 'SCHEDULED_TO_PRIMETIME_SUBSCRIBERS' | 'SPECIFIC_PEOPLE' | + 'SUPERFANS' | 'UNLISTED' | 'UNSPECIFIED'; + +export type VideoUserInflictedVisibility = 'AGE_RESTRICTED' | 'UNSPECIFIED'; + +export type VideoEffectiveVisibility = + 'BLOCKED' | 'FORCED_PRIVATE' | + 'REMOVED' | 'UNSPECIFIED' | + 'UPLOAD_FAILED' | 'UPLOADING_OR_PROCESSING' | + 'USER_CONFIG'; + +export type VideoOrigin = 'LIVESTREAM' | 'STORY' | 'UNKNOWN' | 'UPLOAD'; + +export type VideoProcessingStatus = + 'EDITED' | 'FAILED' | 'PROCESSING' | + 'PROCESSING_NON_PRIMARY_ASSETS' | 'READY' | + 'REVERTED' | 'UNEDITED' | 'UNKNOWN'; + +export type VideoTargetedAudience = + 'ALL' | 'AGE_RESTRICTED' | + 'CROSSWALK' | 'UNKNOWN'; + +export type VideoTargetedAudienceImposer = + 'SELF' | 'SYSTEM' | 'UNSPECIFIED'; + +export type ResolutionStatus = + 'APPEAL_IN_PROGRESS' | 'APPEAL_REJECTED' | 'AVAILABLE' | + 'DEFERRED' | 'DONE' | 'PROCESSING' | + 'STARTING_SOON' | 'UNAVAILABLE' | 'UNKNOWN' | + 'UNSPECIFIED'; + +export type MonetizedStatus = 'ACTIVE' | 'INACTIVE' | 'UNSPECIFIED'; + +export type RestrictionsSeverity = + 'HIGH' | 'LOW' | + 'MEDIUM' | 'UNSPECIFIED'; + +export type AutoGenMidrollsStatus = 'AVAILABLE' | 'FAILED' | 'PROCESSING'; + +export type VideoCopyrightSummaryStatus = + 'BLOCKED' | 'COPYRIGHT_CONTENT_FOUND' | + 'COUNTER_REJECTED' | 'DELAYED_TAKEDOWN' | + 'EXPIRED_STRIKE_TAKEDOWN' | 'LICENSED' | + 'LIKENESS_CLAIM' | 'LIKENESS_PENDING_REMOVAL' | + 'LIKENESS_REMOVAL' | 'MONETIZABLE_WITH_LICENSES' | + 'MONETIZATION_CREATOR_REVSHARE' | 'MONETIZATION_CREATOR_REVSHARE_ELIGIBLE' | + 'MONETIZATION_ENABLED_WITH_LICENSES' | 'MONETIZATION_RESTRICTED' | + 'MONETIZATION_REVSHARE_ELIGIBLE' | 'MONETIZATION_REVSHARE_ENABLED' | + 'MONETIZATION_SHORTS_REVSHARE' | 'MONETIZATION_SHORTS_REVSHARE_ELIGIBLE' | + 'MONETIZATION_UNAVAILABLE' | 'MULTIPLE_CLAIMS' | + 'MULTIPLE_REMOVALS' | 'NO_CLAIMS_FOUND' | + 'PARTIALLY_BLOCKED_REVSHARE_ENABLED' | 'SHORTS_NO_UPLOADER_CLAIM' | + 'STRIKE_TAKEDOWN' | 'TAKEDOWN' | + 'TAKEDOWN_COUNTER' | 'TAKEDOWN_NO_STRIKE' | + 'TAKEDOWN_UNDER_REVIEW' | 'VIDEO_APPEAL' | + 'VIDEO_DISPUTE'; + +export type VideoCopyrightChannelImpact = + 'NO_CHANNEL_IMPACT' | 'NO_CHANNEL_IMPACT_WITH_LICENSES' | + 'NO_CHANNEL_IMPACT_WITHOUT_CLAIMS' | 'PENDING_STRIKE_REVIEW' | + 'STRIKE' | 'STRIKE_COUNTER' | + 'STRIKE_EXPIRED' | 'STRIKE_PENDING' | + 'STRIKE_RELEASED_DURING_COUNTER'; + +export type VideoCopyrightVisibilityImpact = + 'APPEAL' | 'CLAIM_BLOCK' | + 'CLAIM_PARTIAL_BLOCK' | 'COMMERCIAL_SHORTS_BLOCK' | + 'DELAYED_TAKEDOWN' | 'DISPUTE' | + 'LICENSE_RESTRICTED_SHORTS_BLOCK' | 'LIKENESS_BLOCK' | + 'MULTIPLE_CLAIMS_BLOCK' | 'NOT_AFFECTED' | + 'PENDING_LIKENESS_REMOVAL' | 'TAKEDOWN' | + 'TAKEDOWN_COUNTER' | 'UNKNOWN'; + +export type VideoCopyrightMonetizationImpact = + 'CLAIM_BLOCK' | 'CLAIM_PARTIAL_BLOCK' | + 'CLAIM_PARTIAL_BLOCK_MONETIZED' | 'CREATOR_REVSHARE' | + 'CREATOR_REVSHARE_ELIGIBLE' | 'DO_NOT_DISPLAY' | + 'LIKENESS_REVSHARE_ELIGIBLE' | 'LIKENESS_REVSHARE_ENABLED' | + 'MONETIZABLE_WITH_LICENSES' | 'MONETIZED_DURING_DISPUTE' | + 'NOT_AFFECTED' | 'NOT_AFFECTED_LICENSED' | + 'RESTRICTED' | 'REVSHARE_ELIGIBLE' | + 'REVSHARE_ENABLED' | 'SHORTS_NO_UPLOADER_CLAIM' | + 'SHUNA_CLAIM_DEMONETIZATION' | 'TAKEDOWN' | + 'UNKNOWN'; + +export type TouPolicyVertical = + 'CHILD_SAFETY' | 'HARMFUL_DANGEROUS' | 'SUICIDE_SELF_HARM' | + 'UNKNOWN' | 'VIOLENT_GRAPHIC_SHOCKING'; + +export type HumanReviewState = 'DONE' | 'INELIGIBLE' | 'NOT_REQUESTED'; + +export type VideoMadeForKids = 'MFK' | 'NOT_MFK' | 'UNKNOWN'; + +export type VideoMadeForKidsImposer = + 'SELF' | 'UNSPECIFIED' | 'YOUTUBE'; + +export type RemixSourceOptionEligibility = + 'BY_CLIENT' | 'ELIGIBLE' | 'INELIGIBLE'; + +export type RemixSourceShorts = 'IS_SHORT' | 'NOT_SHORT' | 'PROCESSING'; + +export type VideoCommentsEnabledState = + 'OFF' | 'ON' | + 'PAUSED' | 'UNKNOWN'; + +export type AllowedCommenterMode = + 'ANYONE' | 'SUBSCRIBERS_MEMBERS_APPROVED_USERS' | 'UNKNOWN'; + +export type CommenterMinimumSubscriptionTime = + 'ANY' | 'ONE_DAY' | + 'ONE_HOUR' | 'ONE_WEEK' | + 'UNKNOWN'; + +export type CreatorContentType = + 'LIVE_STREAM' | 'SHORTS' | + 'UNSPECIFIED' | 'VIDEO_ON_DEMAND'; + +export type CreatorVideoPermission = + 'ANALYTICS_READ' | 'BASIC_METADATA_READ' | + 'CAPTIONS_READ' | 'CAPTIONS_WRITE' | + 'COLLABORATOR' | 'COLLABORATOR_INVITEE' | + 'COLLABORATOR_LIMITED' | 'COLLABORATOR_LIMITED_INVITEE' | + 'COMMENTS_MANAGER' | 'COMMENTS_READ' | + 'COMMENT_SETTINGS_READ' | 'COMMENT_SETTINGS_WRITE' | + 'DELETE' | 'DOWNLOAD' | + 'ENFORCEMENT_APPELLANT' | 'ENFORCEMENT_READER' | + 'MONETIZATION_SETTINGS_READ' | 'MONETIZATION_WRITE' | + 'PRIVACY_STATUS_PRIVATE_WRITE' | 'PRIVACY_STATUS_PUBLIC_WRITE' | + 'RATING_SETTINGS_WRITE' | 'READ' | + 'WATCH' | 'WRITE'; + +export type CreatorEntityStatus = 'FAILURE' | 'OK' | 'PARTIAL_FAILURE'; + +export type CreatorFeatureStatus = + 'DISABLED' | 'ELIGIBLE' | + 'ENABLED' | 'UNKNOWN'; + +export type CreatorFeatureStatusDetails = 'NOT_APPLICABLE'; + +export interface ClaimDetails { + is_embed_disabled?: boolean; + video_has_commercial_block?: boolean; + video_has_third_party_claim?: boolean; +} + +export interface Permissions { + overall_permissions?: CreatorVideoPermission[]; +} + +export interface ResponseStatus { + status_code?: CreatorEntityStatus; +} + +export interface ThumbnailEditorState { + stills?: Thumbnail[][]; + still_id?: number; + default_still?: boolean; +} + +export interface VideoEditorProject { + video_dimensions?: { width?: number, height?: number }; +} + +export interface StatusDetails { + feedback_service_continuation_token?: string; +} + +export interface VideoTag { + value: string; +} + +export interface AudioLanguage { + language_code?: string; +} + +export interface FeatureState { + status?: CreatorFeatureStatus; + status_details?: CreatorFeatureStatusDetails; +} + +export interface Publishing { + notify_subscribers?: VideoSubscriberNotification; +} + +export interface Music { + licensed_status?: VideoMusicLicensedStatus; +} + +export interface AdMonetization { + user_set_monetization?: VideoUserSetMonetization; + effective_status?: VideoMonetizingStatus; +} + +export interface Monetization { + ad_monetization?: AdMonetization; +} + +export interface VideoVisibility { + effective_status?: VideoVisibilityStatus; + user_set_visibility?: VideoUserSetVisibility; + user_inflicted_visibility?: VideoUserInflictedVisibility; + effective_visibility?: VideoEffectiveVisibility; +} + +export interface CopyrightSummary { + video_copyright_summary_status?: VideoCopyrightSummaryStatus; + channel_impacts?: VideoCopyrightChannelImpact[]; + video_visibility_impacts?: VideoCopyrightVisibilityImpact[]; + video_monetization_impact?: VideoCopyrightMonetizationImpact; + active_third_party_claims_count?: number; +} + +export interface SponsorsOnly { + is_sponsors_only?: boolean; +} + +export interface AudienceRestriction { + self_rating?: VideoTargetedAudience; + system_rating?: VideoTargetedAudience; + override_enabled?: boolean; + effective_rating?: VideoTargetedAudience; + imposer?: VideoTargetedAudienceImposer; +} + +export interface OwnedClaimDetails { + can_edit_owned_claim?: boolean; + can_enable_matching?: boolean; +} + +export interface RestrictionsSummary { + severity?: RestrictionsSeverity; +} + +export interface AllRestrictions { + summary?: RestrictionsSummary; +} + +export interface VideoResolutions { + status_sd?: ResolutionStatus; + status_hd?: ResolutionStatus; + status4k?: ResolutionStatus; + status2k?: ResolutionStatus; +} + +export interface AdFormats { + has_skippable_video_ads?: boolean; + has_non_skippable_video_ads?: boolean; + has_display_ads?: boolean; + has_live_display_ads?: boolean; +} + +export interface AdBreaks { + has_preroll_ads?: boolean; + has_midroll_ads?: boolean; + has_postroll_ads?: boolean; + auto_gen_midrolls_status?: AutoGenMidrollsStatus; +} + +export interface AdSettings { + ad_formats?: AdFormats; + ad_breaks?: AdBreaks; +} + +export interface PolicyDetail { + vertical?: TouPolicyVertical; +} + +export interface CommunityGuidelinesDetails { + all_policy_details?: PolicyDetail[]; + human_review_state?: HumanReviewState; +} + +export interface AdditionalDetails { + community_guidelines_details?: CommunityGuidelinesDetails; +} + +export interface VideoPrechecks { + copyright_prechecks_done?: boolean; + brand_safety_prechecks_done?: boolean; + video_upload_checks_monetized?: VideoUploadChecks; + video_upload_checks_not_monetized?: VideoUploadChecks; + additional_details?: AdditionalDetails; +} + +export interface Notification { + precheck_notifications_enabled?: boolean; +} + +export interface MfkSettings { + mfk_by_creator?: VideoMadeForKids; + mfk_without_creator_input?: VideoMadeForKids; + override_enabled?: boolean; + effective_mfk?: VideoMadeForKids; + imposer?: VideoMadeForKidsImposer; +} + +export interface CreatorOptOutSetting { + creator_opt_out?: boolean; +} + +export interface Remix { + remix_source_option_eligibility?: RemixSourceOptionEligibility; + is_source?: boolean; + remix_source_shorts?: RemixSourceShorts; +} + +export interface ContentOwnershipModelSettings { + is_off_network_upload?: boolean; +} + +export interface PublicMetrics { + view_count?: number; + comment_count?: number; + like_count?: number; + external_view_count?: number; +} + +export interface Shorts { + is_shorts_renderable?: boolean; +} + +export interface CommentSettings { + comments_enabled_state?: VideoCommentsEnabledState; + allowed_commenter_mode?: AllowedCommenterMode; + commenter_minimum_subscription_time?: CommenterMinimumSubscriptionTime; +} + +export interface Collaboration { + serialized_share_entity?: string; +} + +export interface PaidPoliticalContent { + paid_product_placement_political_content_from_eu_creator?: boolean; +} + +export interface SuperfansOnly { + is_superfans_only?: boolean; +} + +export default class CreatorVideo extends YTNode { + static type = 'CreatorVideo'; + + video_id?: string; + channel_id?: string; + title?: string; + description?: string; + privacy?: CreatorVideoPrivacy; + status?: CreatorVideoStatus; + draft_status?: DraftStatus; + share_url?: string; + watch_url?: string; + length_seconds?: number; + video_duration_ms?: number; + time_created_seconds?: number; + time_published_seconds?: number; + thumbnail_details?: Thumbnail[]; + claim_details?: ClaimDetails; + permissions?: Permissions; + response_status?: ResponseStatus; + thumbnail_editor_state?: ThumbnailEditorState; + video_editor_project?: VideoEditorProject; + status_details?: StatusDetails; + tags?: VideoTag[]; + category?: CreatorVideoCategory; + comment_filter?: VideoCommentFilter; + default_comment_sort_order?: VideoCommentSortOrder; + audio_language?: AudioLanguage; + allow_ratings?: boolean; + age_restriction?: VideoAgeRestriction; + license?: VideoLicense; + features?: Record; + uncaptioned_reason?: VideoUncaptionedReason; + publishing?: Publishing; + paid_product_placement?: VideoPaidProductPlacement; + allow_embed?: boolean; + music?: Music; + monetization?: Monetization; + visibility?: VideoVisibility; + origin?: VideoOrigin; + inline_edit_processing_status?: VideoProcessingStatus; + copyright_summary?: CopyrightSummary; + sponsors_only?: SponsorsOnly; + serialized_share_entity?: string; + unlisted_expired?: boolean; + original_filename?: string; + audience_restriction?: AudienceRestriction; + owned_claim_details?: OwnedClaimDetails; + monetized_status?: MonetizedStatus; + comments_disabled_internally?: boolean; + all_restrictions?: AllRestrictions; + video_resolutions?: VideoResolutions; + ad_settings?: AdSettings; + video_prechecks?: VideoPrechecks; + view_count_is_hidden?: boolean; + notification?: Notification; + mfk_settings?: MfkSettings; + auto_chapter_settings?: CreatorOptOutSetting; + remix?: Remix; + content_ownership_model_settings?: ContentOwnershipModelSettings; + public_metrics?: PublicMetrics; + auto_places_mentioned_settings?: CreatorOptOutSetting; + shorts?: Shorts; + content_type?: CreatorContentType; + is_paygated?: boolean; + learning_concept_settings?: CreatorOptOutSetting; + comment_settings?: CommentSettings; + product_autotagging_settings?: CreatorOptOutSetting; + collaboration?: Collaboration; + paid_political_content?: PaidPoliticalContent; + superfans_only?: SuperfansOnly; + + constructor(data: RawNode) { + super(); + + if (Reflect.has(data, 'videoId')) { + this.video_id = data.videoId; + } + + if (Reflect.has(data, 'channelId')) { + this.channel_id = data.channelId; + } + + if (Reflect.has(data, 'title')) { + this.title = data.title; + } + + if (Reflect.has(data, 'description')) { + this.description = data.description; + } + + if (Reflect.has(data, 'privacy')) { + this.privacy = data.privacy.replace('VIDEO_PRIVACY_', ''); + } + + if (Reflect.has(data, 'status')) { + this.status = data.status.replace('VIDEO_STATUS_', ''); + } + + if (Reflect.has(data, 'draftStatus')) { + this.draft_status = data.draftStatus.replace('DRAFT_STATUS_', ''); + } + + if (Reflect.has(data, 'shareUrl')) { + this.share_url = data.shareUrl; + } + + if (Reflect.has(data, 'watchUrl')) { + this.watch_url = data.watchUrl; + } + + if (Reflect.has(data, 'lengthSeconds')) { + this.length_seconds = Number(data.lengthSeconds); + } + + if (Reflect.has(data, 'videoDurationMs')) { + this.video_duration_ms = Number(data.videoDurationMs); + } + + if (Reflect.has(data, 'timeCreatedSeconds')) { + this.time_created_seconds = Number(data.timeCreatedSeconds); + } + + if (Reflect.has(data, 'timePublishedSeconds')) { + this.time_published_seconds = Number(data.timePublishedSeconds); + } + + if (Reflect.has(data, 'thumbnailDetails')) { + this.thumbnail_details = Thumbnail.fromResponse(data.thumbnailDetails); + } + + if (Reflect.has(data, 'claimDetails')) { + const claim_details = data.claimDetails; + this.claim_details = { + is_embed_disabled: claim_details.isEmbedDisabled, + video_has_commercial_block: claim_details.videoHasCommercialBlock, + video_has_third_party_claim: claim_details.videoHasThirdPartyClaim + }; + } + + if (Reflect.has(data, 'permissions')) { + const permissions = data.permissions; + this.permissions = { + overall_permissions: Array.isArray(permissions.overallPermissions) ? + permissions.overallPermissions.map((permission: string) => permission.replace('CREATOR_VIDEO_PERMISSION_', '')) : undefined + }; + } + + if (Reflect.has(data, 'responseStatus')) { + const response_status = data.responseStatus; + this.response_status = { + status_code: Reflect.has(response_status, 'statusCode') ? response_status.statusCode.replace('CREATOR_ENTITY_STATUS_', '') : undefined + }; + } + + if (Reflect.has(data, 'thumbnailEditorState')) { + const thumbnail_editor_state = data.thumbnailEditorState; + this.thumbnail_editor_state = { + stills: Array.isArray(thumbnail_editor_state.stills) ? thumbnail_editor_state.stills.map((still: RawNode) => Thumbnail.fromResponse(still)) : undefined, + still_id: thumbnail_editor_state.stillId, + default_still: thumbnail_editor_state.defaultStill + }; + } + + if (Reflect.has(data, 'videoEditorProject')) { + const video_dimensions = data.videoEditorProject.videoDimensions; + this.video_editor_project = { + video_dimensions: video_dimensions ? { + width: video_dimensions.width, + height: video_dimensions.height + } : undefined + }; + } + + if (Reflect.has(data, 'statusDetails')) { + this.status_details = { + feedback_service_continuation_token: data.statusDetails.feedbackServiceContinuationToken + }; + } + + if (Reflect.has(data, 'tags')) { + this.tags = Array.isArray(data.tags) ? data.tags.map((tag: RawNode) => ({ value: tag.value })) : undefined; + } + + if (Reflect.has(data, 'category')) { + this.category = data.category.replace('CREATOR_VIDEO_CATEGORY_', ''); + } + + if (Reflect.has(data, 'commentFilter')) { + this.comment_filter = data.commentFilter.replace('VIDEO_COMMENT_FILTER_', ''); + } + + if (Reflect.has(data, 'defaultCommentSortOrder')) { + this.default_comment_sort_order = data.defaultCommentSortOrder.replace('VIDEO_COMMENT_SORT_ORDER_', ''); + } + + if (Reflect.has(data, 'audioLanguage')) { + this.audio_language = { + language_code: data.audioLanguage.languageCode + }; + } + + if (Reflect.has(data, 'allowRatings')) { + this.allow_ratings = data.allowRatings; + } + + if (Reflect.has(data, 'ageRestriction')) { + this.age_restriction = data.ageRestriction.replace('VIDEO_AGE_RESTRICTION_', ''); + } + + if (Reflect.has(data, 'license')) { + this.license = data.license.replace('VIDEO_LICENSE_', ''); + } + + if (Reflect.has(data, 'features')) { + const features: Record = {}; + for (const [ key, value ] of Object.entries(data.features)) { + features[key] = { + status: Reflect.has(value, 'status') ? value.status.replace('CREATOR_FEATURE_STATUS_', '') : undefined, + status_details: Reflect.has(value, 'statusDetails') ? value.statusDetails.replace('CREATOR_FEATURE_STATUS_DETAILS_', '') : undefined + }; + } + this.features = features; + } + + if (Reflect.has(data, 'uncaptionedReason')) { + this.uncaptioned_reason = data.uncaptionedReason.replace('VIDEO_UNCAPTIONED_REASON_', ''); + } + + if (Reflect.has(data, 'publishing')) { + this.publishing = { + notify_subscribers: Reflect.has(data.publishing, 'notifySubscribers') ? data.publishing.notifySubscribers.replace('VIDEO_SUBSCRIBER_NOTIFICATION_', '') : undefined + }; + } + + if (Reflect.has(data, 'paidProductPlacement')) { + this.paid_product_placement = data.paidProductPlacement.replace('VIDEO_PAID_PRODUCT_PLACEMENT_', ''); + } + + if (Reflect.has(data, 'allowEmbed')) { + this.allow_embed = data.allowEmbed; + } + + if (Reflect.has(data, 'music')) { + this.music = { + licensed_status: Reflect.has(data.music, 'licensedStatus') ? data.music.licensedStatus.replace('VIDEO_MUSIC_LICENSED_STATUS_', '') : undefined + }; + } + + if (Reflect.has(data, 'monetization')) { + const ad_monetization = data.monetization.adMonetization; + this.monetization = { + ad_monetization: ad_monetization ? { + user_set_monetization: Reflect.has(ad_monetization, 'userSetMonetization') ? ad_monetization.userSetMonetization.replace('VIDEO_USER_SET_MONETIZATION_', '') : undefined, + effective_status: Reflect.has(ad_monetization, 'effectiveStatus') ? ad_monetization.effectiveStatus.replace('VIDEO_MONETIZING_STATUS_', '') : undefined + } : undefined + }; + } + + if (Reflect.has(data, 'visibility')) { + const visibility = data.visibility; + this.visibility = { + effective_status: Reflect.has(visibility, 'effectiveStatus') ? visibility.effectiveStatus.replace('VIDEO_VISIBILITY_STATUS_', '') : undefined, + user_set_visibility: Reflect.has(visibility, 'userSetVisibility') ? visibility.userSetVisibility.replace('VIDEO_USER_SET_VISIBILITY_', '') : undefined, + user_inflicted_visibility: Reflect.has(visibility, 'userInflictedVisibility') ? visibility.userInflictedVisibility.replace('VIDEO_USER_INFLICTED_VISIBILITY_', '') : undefined, + effective_visibility: Reflect.has(visibility, 'effectiveVisibility') ? visibility.effectiveVisibility.replace('VIDEO_EFFECTIVE_VISIBILITY_', '') : undefined + }; + } + + if (Reflect.has(data, 'origin')) { + this.origin = data.origin.replace('VIDEO_ORIGIN_', ''); + } + + if (Reflect.has(data, 'inlineEditProcessingStatus')) { + this.inline_edit_processing_status = data.inlineEditProcessingStatus.replace('VIDEO_PROCESSING_STATUS_', ''); + } + + if (Reflect.has(data, 'copyrightSummary')) { + const copyright_summary = data.copyrightSummary; + this.copyright_summary = { + video_copyright_summary_status: Reflect.has(copyright_summary, 'videoCopyrightSummaryStatus') ? copyright_summary.videoCopyrightSummaryStatus.replace('VIDEO_COPYRIGHT_SUMMARY_STATUS_', '') : undefined, + channel_impacts: Array.isArray(copyright_summary.channelImpacts) ? + copyright_summary.channelImpacts.map((impact: string) => impact.replace('VIDEO_COPYRIGHT_CHANNEL_IMPACT_', '')) : undefined, + video_visibility_impacts: Array.isArray(copyright_summary.videoVisibilityImpacts) ? + copyright_summary.videoVisibilityImpacts.map((impact: string) => impact.replace('VIDEO_COPYRIGHT_VISIBILITY_IMPACT_', '')) : undefined, + video_monetization_impact: Reflect.has(copyright_summary, 'videoMonetizationImpact') ? copyright_summary.videoMonetizationImpact.replace('VIDEO_COPYRIGHT_MONETIZATION_IMPACT_', '') : undefined, + active_third_party_claims_count: copyright_summary.activeThirdPartyClaimsCount + }; + } + + if (Reflect.has(data, 'sponsorsOnly')) { + this.sponsors_only = { + is_sponsors_only: data.sponsorsOnly.isSponsorsOnly + }; + } + + if (Reflect.has(data, 'serializedShareEntity')) { + this.serialized_share_entity = data.serializedShareEntity; + } + + if (Reflect.has(data, 'unlistedExpired')) { + this.unlisted_expired = data.unlistedExpired; + } + + if (Reflect.has(data, 'originalFilename')) { + this.original_filename = data.originalFilename; + } + + if (Reflect.has(data, 'audienceRestriction')) { + const audience_restriction = data.audienceRestriction; + this.audience_restriction = { + self_rating: Reflect.has(audience_restriction, 'selfRating') ? audience_restriction.selfRating.replace('VIDEO_TARGETED_AUDIENCE_', '') : undefined, + system_rating: Reflect.has(audience_restriction, 'systemRating') ? audience_restriction.systemRating.replace('VIDEO_TARGETED_AUDIENCE_', '') : undefined, + override_enabled: audience_restriction.overrideEnabled, + effective_rating: Reflect.has(audience_restriction, 'effectiveRating') ? audience_restriction.effectiveRating.replace('VIDEO_TARGETED_AUDIENCE_', '') : undefined, + imposer: Reflect.has(audience_restriction, 'imposer') ? audience_restriction.imposer.replace('VIDEO_TARGETED_AUDIENCE_IMPOSER_', '') : undefined + }; + } + + if (Reflect.has(data, 'ownedClaimDetails')) { + const owned_claim_details = data.ownedClaimDetails; + this.owned_claim_details = { + can_edit_owned_claim: owned_claim_details.canEditOwnedClaim, + can_enable_matching: owned_claim_details.canEnableMatching + }; + } + + if (Reflect.has(data, 'monetizedStatus')) { + this.monetized_status = data.monetizedStatus.replace('MONETIZED_STATUS_', ''); + } + + if (Reflect.has(data, 'commentsDisabledInternally')) { + this.comments_disabled_internally = data.commentsDisabledInternally; + } + + if (Reflect.has(data, 'allRestrictions')) { + const summary = data.allRestrictions.summary; + this.all_restrictions = { + summary: summary ? { + severity: Reflect.has(summary, 'severity') ? summary.severity.replace('VIDEO_RESTRICTIONS_SEVERITY_', '') : undefined + } : undefined + }; + } + + if (Reflect.has(data, 'videoResolutions')) { + const video_resolutions = data.videoResolutions; + this.video_resolutions = { + status_sd: Reflect.has(video_resolutions, 'statusSd') ? video_resolutions.statusSd.replace('RESOLUTION_STATUS_', '') : undefined, + status_hd: Reflect.has(video_resolutions, 'statusHd') ? video_resolutions.statusHd.replace('RESOLUTION_STATUS_', '') : undefined, + status4k: Reflect.has(video_resolutions, 'status4k') ? video_resolutions.status4k.replace('RESOLUTION_STATUS_', '') : undefined, + status2k: Reflect.has(video_resolutions, 'status2k') ? video_resolutions.status2k.replace('RESOLUTION_STATUS_', '') : undefined + }; + } + + if (Reflect.has(data, 'adSettings')) { + const ad_formats = data.adSettings.adFormats; + const ad_breaks = data.adSettings.adBreaks; + this.ad_settings = { + ad_formats: ad_formats ? { + has_skippable_video_ads: ad_formats.hasSkippableVideoAds, + has_non_skippable_video_ads: ad_formats.hasNonSkippableVideoAds, + has_display_ads: ad_formats.hasDisplayAds, + has_live_display_ads: ad_formats.hasLiveDisplayAds + } : undefined, + ad_breaks: ad_breaks ? { + has_preroll_ads: ad_breaks.hasPrerollAds, + has_midroll_ads: ad_breaks.hasMidrollAds, + has_postroll_ads: ad_breaks.hasPostrollAds, + auto_gen_midrolls_status: Reflect.has(ad_breaks, 'autoGenMidrollsStatus') ? ad_breaks.autoGenMidrollsStatus.replace('AUTO_GEN_MIDROLLS_STATUS_', '') : undefined + } : undefined + }; + } + + if (Reflect.has(data, 'videoPrechecks')) { + const raw_prechecks = data.videoPrechecks; + const community_guidelines_details = raw_prechecks.additionalDetails?.communityGuidelinesDetails; + this.video_prechecks = { + copyright_prechecks_done: raw_prechecks.copyrightPrechecksDone, + brand_safety_prechecks_done: raw_prechecks.brandSafetyPrechecksDone, + video_upload_checks_monetized: Reflect.has(raw_prechecks, 'videoUploadChecksMonetized') ? new VideoUploadChecks(raw_prechecks.videoUploadChecksMonetized) : undefined, + video_upload_checks_not_monetized: Reflect.has(raw_prechecks, 'videoUploadChecksNotMonetized') ? new VideoUploadChecks(raw_prechecks.videoUploadChecksNotMonetized) : undefined, + additional_details: Reflect.has(raw_prechecks, 'additionalDetails') ? { + community_guidelines_details: community_guidelines_details ? { + all_policy_details: Array.isArray(community_guidelines_details.allPolicyDetails) ? + community_guidelines_details.allPolicyDetails.map((policy_detail: RawNode) => ({ + vertical: Reflect.has(policy_detail, 'vertical') ? policy_detail.vertical.replace('TOU_POLICY_VERTICAL_', '') : undefined + })) : undefined, + human_review_state: Reflect.has(community_guidelines_details, 'humanReviewState') ? community_guidelines_details.humanReviewState.replace('HUMAN_REVIEW_STATE_', '') : undefined + } : undefined + } : undefined + }; + } + + if (Reflect.has(data, 'viewCountIsHidden')) { + this.view_count_is_hidden = data.viewCountIsHidden; + } + + if (Reflect.has(data, 'notification')) { + this.notification = { + precheck_notifications_enabled: data.notification.precheckNotificationsEnabled + }; + } + + if (Reflect.has(data, 'mfkSettings')) { + const mfk_settings = data.mfkSettings; + this.mfk_settings = { + mfk_by_creator: Reflect.has(mfk_settings, 'mfkByCreator') ? mfk_settings.mfkByCreator.replace('VIDEO_MADE_FOR_KIDS_', '') : undefined, + mfk_without_creator_input: Reflect.has(mfk_settings, 'mfkWithoutCreatorInput') ? mfk_settings.mfkWithoutCreatorInput.replace('VIDEO_MADE_FOR_KIDS_', '') : undefined, + override_enabled: mfk_settings.overrideEnabled, + effective_mfk: Reflect.has(mfk_settings, 'effectiveMfk') ? mfk_settings.effectiveMfk.replace('VIDEO_MADE_FOR_KIDS_', '') : undefined, + imposer: Reflect.has(mfk_settings, 'imposer') ? mfk_settings.imposer.replace('VIDEO_MADE_FOR_KIDS_IMPOSER_', '') : undefined + }; + } + + if (Reflect.has(data, 'autoChapterSettings')) { + this.auto_chapter_settings = { + creator_opt_out: data.autoChapterSettings.creatorOptOut + }; + } + + if (Reflect.has(data, 'remix')) { + const remix = data.remix; + this.remix = { + remix_source_option_eligibility: Reflect.has(remix, 'remixSourceOptionEligibility') ? remix.remixSourceOptionEligibility.replace('REMIX_SOURCE_OPTION_ELIGIBILITY_', '') : undefined, + is_source: remix.isSource, + remix_source_shorts: Reflect.has(remix, 'remixSourceShorts') ? remix.remixSourceShorts.replace('REMIX_SOURCE_SHORTS_', '') : undefined + }; + } + + if (Reflect.has(data, 'contentOwnershipModelSettings')) { + this.content_ownership_model_settings = { + is_off_network_upload: data.contentOwnershipModelSettings.isOffNetworkUpload + }; + } + + if (Reflect.has(data, 'publicMetrics')) { + const public_metrics = data.publicMetrics; + this.public_metrics = { + view_count: Number(public_metrics.viewCount), + comment_count: Number(public_metrics.commentCount), + like_count: Number(public_metrics.likeCount), + external_view_count: Number(public_metrics.externalViewCount) + }; + } + + if (Reflect.has(data, 'autoPlacesMentionedSettings')) { + this.auto_places_mentioned_settings = { + creator_opt_out: data.autoPlacesMentionedSettings.creatorOptOut + }; + } + + if (Reflect.has(data, 'shorts')) { + this.shorts = { + is_shorts_renderable: data.shorts.isShortsRenderable + }; + } + + if (Reflect.has(data, 'contentType')) { + this.content_type = data.contentType.replace('CREATOR_CONTENT_TYPE_', ''); + } + + if (Reflect.has(data, 'isPaygated')) { + this.is_paygated = data.isPaygated; + } + + if (Reflect.has(data, 'learningConceptSettings')) { + this.learning_concept_settings = { + creator_opt_out: data.learningConceptSettings.creatorOptOut + }; + } + + if (Reflect.has(data, 'commentSettings')) { + const comment_settings = data.commentSettings; + this.comment_settings = { + comments_enabled_state: Reflect.has(comment_settings, 'commentsEnabledState') ? comment_settings.commentsEnabledState.replace('VIDEO_COMMENTS_ENABLED_STATE_', '') : undefined, + allowed_commenter_mode: Reflect.has(comment_settings, 'allowedCommenterMode') ? comment_settings.allowedCommenterMode.replace('ALLOWED_COMMENTER_MODE_', '') : undefined, + commenter_minimum_subscription_time: Reflect.has(comment_settings, 'commenterMinimumSubscriptionTime') ? comment_settings.commenterMinimumSubscriptionTime.replace('COMMENTER_MINIMUM_SUBSCRIPTION_TIME_', '') : undefined + }; + } + + if (Reflect.has(data, 'productAutotaggingSettings')) { + this.product_autotagging_settings = { + creator_opt_out: data.productAutotaggingSettings.creatorOptOut + }; + } + + if (Reflect.has(data, 'collaboration')) { + this.collaboration = { + serialized_share_entity: data.collaboration.serializedShareEntity + }; + } + + if (Reflect.has(data, 'paidPoliticalContent')) { + this.paid_political_content = { + paid_product_placement_political_content_from_eu_creator: data.paidPoliticalContent.paidProductPlacementPoliticalContentFromEuCreator + }; + } + + if (Reflect.has(data, 'superfansOnly')) { + this.superfans_only = { + is_superfans_only: data.superfansOnly.isSuperfansOnly + }; + } + } +} diff --git a/src/parser/classes/ytstudio/DataFreshnessEntity.ts b/src/parser/classes/ytstudio/DataFreshnessEntity.ts new file mode 100644 index 0000000000..e9e2fd3b08 --- /dev/null +++ b/src/parser/classes/ytstudio/DataFreshnessEntity.ts @@ -0,0 +1,15 @@ +import { YTNode } from '../../helpers.js'; +import { type RawNode } from '../../index.js'; + +export default class DataFreshnessEntity extends YTNode { + static type = 'DataFreshnessEntity'; + + entity_key: string; + last_updated: { seconds: string, nanos: number }; + + constructor(data: RawNode) { + super(); + this.entity_key = data.key; + this.last_updated = data.lastUpdated; + } +} diff --git a/src/parser/classes/ytstudio/Translation.ts b/src/parser/classes/ytstudio/Translation.ts new file mode 100644 index 0000000000..a5a4cc25cd --- /dev/null +++ b/src/parser/classes/ytstudio/Translation.ts @@ -0,0 +1,103 @@ +import { YTNode } from '../../helpers.js'; +import { type RawNode } from '../../index.js'; + +export type TranslationStatus = + 'DELETING' | 'DRAFT' | 'FAILED' | + 'PROCESSING' | 'PUBLISHED' | 'PUBLISHING' | + 'QC_FAILED' | 'REVIEW' | 'SUBMITTED' | + 'SUCCESS' | 'SYNCING_DRAFT' | 'UNKNOWN'; + +export type TranslationSource = 'CREATOR' | 'COMMUNITY' | 'AUTOMATIC'; + +export interface TTSTrackId { + kind: string; + lang: string; + name: string; +} + +export interface Segment { + start_time_ms: string; + duration_ms: string; + text: string; +} + +export interface CaptionSegments { + segments: Segment[]; +} + +export interface CaptionsTranslation { + status?: TranslationStatus; + source?: TranslationSource; + time_updated_seconds?: number; + tts_track_id?: TTSTrackId; + caption_segments?: CaptionSegments; + content_update_time?: number; + is_complex_track?: boolean; +} + +export default class Translation extends YTNode { + static type = 'Translation'; + + display_name?: string; + language_code?: string; + captions_translations?: CaptionsTranslation[]; + + constructor(data: RawNode) { + super(); + + if (Reflect.has(data, 'displayName')) { + this.display_name = data.displayName; + } + + if (Reflect.has(data, 'languageCode')) { + this.language_code = data.languageCode; + } + + if (Reflect.has(data, 'captionsTranslations')) { + this.captions_translations = data.captionsTranslations.map((captions_translation: RawNode) => { + const parsed_captions_translation: CaptionsTranslation = {}; + + if (Reflect.has(captions_translation, 'status')) { + parsed_captions_translation.status = captions_translation.status.replace('TRANSLATION_STATUS_', ''); + } + + if (Reflect.has(captions_translation, 'source')) { + parsed_captions_translation.source = captions_translation.source.replace('TRANSLATION_SOURCE_', ''); + } + + if (Reflect.has(captions_translation, 'contentUpdateTime')) { + parsed_captions_translation.content_update_time = Number(captions_translation.contentUpdateTime); + } + + if (Reflect.has(captions_translation, 'timeUpdatedSeconds')) { + parsed_captions_translation.time_updated_seconds = Number(captions_translation.timeUpdatedSeconds); + } + + if (Reflect.has(captions_translation, 'ttsTrackId')) { + parsed_captions_translation.tts_track_id = { + kind: captions_translation.ttsTrackId.kind, + lang: captions_translation.ttsTrackId.lang, + name: captions_translation.ttsTrackId.name + }; + } + + if (Reflect.has(captions_translation, 'captionSegments') && Array.isArray(captions_translation.captionSegments?.segments)) { + parsed_captions_translation.caption_segments = { + segments: + captions_translation.captionSegments.segments.map((segment: RawNode) => ({ + start_time_ms: segment.startTimeMs, + duration_ms: segment.durationMs, + text: segment.text + })) + }; + } + + if (Reflect.has(captions_translation, 'isComplexTrack')) { + parsed_captions_translation.is_complex_track = captions_translation.isComplexTrack; + } + + return parsed_captions_translation; + }); + } + } +} diff --git a/src/parser/classes/ytstudio/UploadFeedbackItem.ts b/src/parser/classes/ytstudio/UploadFeedbackItem.ts new file mode 100644 index 0000000000..2a9ad1287f --- /dev/null +++ b/src/parser/classes/ytstudio/UploadFeedbackItem.ts @@ -0,0 +1,104 @@ +import { YTNode } from '../../helpers.js'; +import { type RawNode } from '../../index.js'; +import DataFreshnessEntity from './DataFreshnessEntity.js'; +import VideoUploadChecks from './VideoUploadChecks.js'; + +export interface TimedContinuationData { + timeout_ms: number; + continuation: string; +} + +export interface UploadFeedbackRefreshContinuation { + continuation: string; + continue_in_ms: number; +} + +export interface UploadFeedbackContinuation { + timed_continuation_data?: TimedContinuationData; + upload_feedback_refresh_continuation?: UploadFeedbackRefreshContinuation; +} + +export interface TransferProgressBar { + fraction_completed?: number; + progress_message?: string; +} + +export interface UploadChecksRenderer { + checks_data_video_monetized?: VideoUploadChecks; + checks_data_video_not_monetized?: VideoUploadChecks; +} + +export default class UploadFeedbackItem extends YTNode { + static type = 'UploadFeedbackItem'; + + frontend_upload_id?: string; + video_id?: string; + continuations?: UploadFeedbackContinuation; + data_freshness_entity?: DataFreshnessEntity; + transfer_progress_bar?: TransferProgressBar; + is_processing?: boolean; + upload_checks?: UploadChecksRenderer; + + constructor(data: RawNode) { + super(); + + if (Reflect.has(data, 'id')) { + this.frontend_upload_id = data.id.frontendUploadId; + this.video_id = data.id.videoId; + } + + this.is_processing = false; + if (Reflect.has(data, 'contents') && Array.isArray(data.contents)) { + data.contents.forEach((item: RawNode) => { + if (Reflect.has(item, 'transferProgressBar')) { + this.transfer_progress_bar = { + fraction_completed: item.transferProgressBar.fractionCompleted, + progress_message: item.transferProgressBar.progressMessage?.simpleText + }; + } + + if (Reflect.has(item, 'uploadChecksRenderer')) { + const renderer = item.uploadChecksRenderer; + this.upload_checks = { + checks_data_video_monetized: Reflect.has(renderer, 'checksDataVideoMonetized') ? new VideoUploadChecks(renderer.checksDataVideoMonetized) : undefined, + checks_data_video_not_monetized: Reflect.has(renderer, 'checksDataVideoNotMonetized') ? new VideoUploadChecks(renderer.checksDataVideoNotMonetized) : undefined + }; + } + + if (Reflect.has(item, 'processingResolutionsStatusRenderer')) { + this.is_processing = true; + } + }); + } + if (Reflect.has(data, 'continuations')) { + const timed_continuation_data = data.continuations[0]?.timedContinuationData; + const upload_feedback_refresh_continuation_data = data.continuations[1]?.uploadFeedbackRefreshContinuation; + this.continuations = { + timed_continuation_data: { + continuation: timed_continuation_data.continuation, + timeout_ms: timed_continuation_data.timeoutMs + }, + upload_feedback_refresh_continuation: { + continuation: upload_feedback_refresh_continuation_data.continuation, + continue_in_ms: upload_feedback_refresh_continuation_data.continueInMs + } + }; + } + if (Reflect.has(data, 'dataFreshnessEntity')) { + this.data_freshness_entity = new DataFreshnessEntity(data.dataFreshnessEntity); + } + } + + get continuation_token(): string | null { + const continuation = this.continuations; + return this.continuations?.upload_feedback_refresh_continuation?.continuation ?? + this.continuations?.timed_continuation_data?.continuation ?? + null; + } + + get continuation_delay_ms(): number | null { + return this.continuations?.upload_feedback_refresh_continuation?.continue_in_ms ?? + this.continuations?.timed_continuation_data?.timeout_ms ?? + null; + } +} diff --git a/src/parser/classes/ytstudio/VideoUploadChecks.ts b/src/parser/classes/ytstudio/VideoUploadChecks.ts new file mode 100644 index 0000000000..610a440843 --- /dev/null +++ b/src/parser/classes/ytstudio/VideoUploadChecks.ts @@ -0,0 +1,59 @@ +import { YTNode } from '../../helpers.js'; +import { type RawNode } from '../../index.js'; + +export type UploadChecksSummaryStatus = + 'COMPLETED' | 'EXTENDED_CHECK_STARTED' | + 'INLINE_EDIT_IN_PROGRESS' | 'NOT_STARTED' | + 'OVERDUE' | 'SELF_CERTIFICATION_MISSING' | + 'SHORTS_NOT_ELIGIBLE' | 'STARTED' | + 'UNABLE_TO_RUN' | 'UNKNOWN'; + +export type UploadChecksCopyrightStatus = + 'COMPLETED' | 'INLINE_EDIT_IN_PROGRESS' | + 'NOT_STARTED' | 'OVERDUE' | + 'SHORTS_NOT_ELIGIBLE' | 'STARTED' | + 'UNABLE_TO_RUN' | 'UNKNOWN'; + +export type UploadChecksAdSuitabilityStatus = + 'CHANNEL_NOT_MONETIZED' | 'COMPLETED' | + 'EXTENDED_CHECK_STARTED' | 'NOT_STARTED' | + 'OVERDUE' | 'SELF_CERTIFICATION_MISSING' | + 'SHORTS_NOT_ELIGIBLE' | 'STARTED' | + 'UNABLE_TO_RUN' | 'UNKNOWN' | + 'VIDEO_NOT_MONETIZED'; + +export type UploadChecksCommunityGuidelinesStatus = + 'COMPLETED' | 'NOT_AVAILABLE_ON_PUBLISHED_VIDEO' | + 'NOT_ELIGIBLE' | 'NOT_STARTED' | + 'OVERDUE' | 'SHORTS_NOT_ELIGIBLE' | + 'STARTED' | 'UNABLE_TO_RUN' | + 'UNKNOWN'; + +export default class VideoUploadChecks extends YTNode { + static type = 'VideoUploadChecks'; + + checks_summary?: UploadChecksSummaryStatus; + copyright_check?: UploadChecksCopyrightStatus; + ad_suitability_check?: UploadChecksAdSuitabilityStatus; + community_guidelines_check?: UploadChecksCommunityGuidelinesStatus; + + constructor(data: RawNode) { + super(); + + if (Reflect.has(data, 'checksSummary') && Reflect.has(data.checksSummary, 'status')) { + this.checks_summary = data.checksSummary.status.replace('UPLOAD_CHECKS_DATA_SUMMARY_STATUS_', ''); + } + + if (Reflect.has(data, 'copyrightCheck') && Reflect.has(data.copyrightCheck, 'checkStatus')) { + this.copyright_check = data.copyrightCheck.checkStatus.replace('UPLOAD_CHECKS_DATA_COPYRIGHT_STATUS_', ''); + } + + if (Reflect.has(data, 'adSuitabilityCheck') && Reflect.has(data.adSuitabilityCheck, 'checkStatus')) { + this.ad_suitability_check = data.adSuitabilityCheck.checkStatus.replace('UPLOAD_CHECKS_DATA_AD_SUITABILITY_STATUS_', ''); + } + + if (Reflect.has(data, 'communityGuidelinesCheck') && Reflect.has(data.communityGuidelinesCheck, 'checkStatus')) { + this.community_guidelines_check = data.communityGuidelinesCheck.checkStatus.replace('UPLOAD_CHECKS_DATA_COMMUNITY_GUIDELINES_STATUS_', ''); + } + } +} diff --git a/src/parser/nodes.ts b/src/parser/nodes.ts index 21bd60f064..4d95683ff3 100644 --- a/src/parser/nodes.ts +++ b/src/parser/nodes.ts @@ -559,3 +559,8 @@ export { default as KidsBlocklistPickerItem } from './classes/ytkids/KidsBlockli export { default as KidsCategoriesHeader } from './classes/ytkids/KidsCategoriesHeader.js'; export { default as KidsCategoryTab } from './classes/ytkids/KidsCategoryTab.js'; export { default as KidsHomeScreen } from './classes/ytkids/KidsHomeScreen.js'; +export { default as CreatorVideo } from './classes/ytstudio/CreatorVideo.js'; +export { default as DataFreshnessEntity } from './classes/ytstudio/DataFreshnessEntity.js'; +export { default as Translation } from './classes/ytstudio/Translation.js'; +export { default as UploadFeedbackItem } from './classes/ytstudio/UploadFeedbackItem.js'; +export { default as VideoUploadChecks } from './classes/ytstudio/VideoUploadChecks.js'; diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 014753f489..7ed052b04e 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -41,6 +41,9 @@ import CommentView from './classes/comments/CommentView.js'; import MusicThumbnail from './classes/MusicThumbnail.js'; import OpenPopupAction from './classes/actions/OpenPopupAction.js'; import AppendContinuationItemsAction from './classes/actions/AppendContinuationItemsAction.js'; +import UploadFeedbackItem from './classes/ytstudio/UploadFeedbackItem.js'; +import CreatorVideo from './classes/ytstudio/CreatorVideo.js'; +import Translation from './classes/ytstudio/Translation.js'; import type { IParsedResponse, IRawResponse, RawData, RawNode } from './types/index.js'; const TAG = 'Parser'; @@ -277,6 +280,15 @@ export function parseResponse(data: } _clearMemo(); + _createMemo(); + const continuation_contents_array = data.continuationContents && Array.isArray(data.continuationContents) ? data.continuationContents.map((item) => parseLC(item)) : null; + const continuation_contents_array_memo = _getMemo(); + if (continuation_contents_array) { + parsed_data.continuation_contents_array = continuation_contents_array; + parsed_data.continuation_contents_array_memo = continuation_contents_array_memo; + } + _clearMemo(); + _createMemo(); const actions = data.actions ? parseActions(data.actions) : null; const actions_memo = _getMemo(); @@ -507,6 +519,42 @@ export function parseResponse(data: parsed_data.challenge = data.challenge; } + if (data.botguardData) { + const interpreter_url = { + private_do_not_access_or_else_trusted_resource_url_wrapped_value: data.botguardData.interpreterSafeUrl?.privateDoNotAccessOrElseTrustedResourceUrlWrappedValue, + private_do_not_access_or_else_safe_script_wrapped_value: data.botguardData.interpreterSafeUrl?.privateDoNotAccessOrElseSafeScriptWrappedValue + }; + + parsed_data.botguard_data = { + interpreter_url, + program: data.botguardData.program + }; + } + + if (data.eats) { + parsed_data.eats = data.eats; + } + + if (data.ctx) { + parsed_data.ctx = data.ctx; + } + + if (data.shouldFetchReauthSessionToken !== undefined) { + parsed_data.should_fetch_reauth_session_token = data.shouldFetchReauthSessionToken; + } + + if (data.encodedReauthProofToken) { + parsed_data.encoded_reauth_proof_token = data.encodedReauthProofToken; + } + + if (data.sessionRiskCtx) { + parsed_data.session_risk_ctx = data.sessionRiskCtx; + } + + if (data.sessionToken) { + parsed_data.session_token = data.sessionToken; + } + if (data.playerResponse) { parsed_data.player_response = parseResponse(data.playerResponse); } @@ -530,6 +578,27 @@ export function parseResponse(data: parsed_data.target_id = data.targetId; } + if (data.videoId) { + parsed_data.video_id = data.videoId; + } + + if (data.translation) { + parsed_data.translation = new Translation(data.translation); + } + + if (data?.creatorEntities?.wrappedVideoData?.video) { + parsed_data.creator_video = new CreatorVideo(data.creatorEntities.wrappedVideoData.video); + } + + if (data.feedbackResponses) { + parsed_data.feedback_responses = data.feedbackResponses; + } + + const challenge_prompt_type = data.responseContext?.webResponseContextExtensionData?.challenge?.type; + if (challenge_prompt_type) { + parsed_data.challenge_prompt_type = challenge_prompt_type; + } + return parsed_data; } @@ -724,7 +793,11 @@ export function parseC(data: RawNode) { return null; } -export function parseLC(data: RawNode) { +export type ContinuationContents = null | ItemSectionContinuation | SectionListContinuation | LiveChatContinuation | + MusicPlaylistShelfContinuation | MusicShelfContinuation | GridContinuation | + PlaylistPanelContinuation | ContinuationCommand | UploadFeedbackItem; +export function parseLC(data: RawNode): ContinuationContents +export function parseLC(data: RawNode): ContinuationContents { if (data.itemSectionContinuation) return new ItemSectionContinuation(data.itemSectionContinuation); if (data.sectionListContinuation) @@ -741,7 +814,8 @@ export function parseLC(data: RawNode) { return new PlaylistPanelContinuation(data.playlistPanelContinuation); if (data.continuationCommand) return new ContinuationCommand(data.continuationCommand); - + if (data.uploadFeedbackItemContinuation) + return new UploadFeedbackItem(data.uploadFeedbackItemContinuation); return null; } diff --git a/src/parser/types/ParsedResponse.ts b/src/parser/types/ParsedResponse.ts index cd19e006b5..48e46ae57b 100644 --- a/src/parser/types/ParsedResponse.ts +++ b/src/parser/types/ParsedResponse.ts @@ -4,6 +4,8 @@ import type { ItemSectionContinuation, LiveChatContinuation, MusicPlaylistShelfContinuation, MusicShelfContinuation, PlaylistPanelContinuation, SectionListContinuation, ContinuationCommand, ShowMiniplayerCommand, NavigateAction } from '../index.js'; +import type Translation from '../classes/ytstudio/Translation.js'; +import type CreatorVideo from '../classes/ytstudio/CreatorVideo.js'; import type PlayerCaptionsTracklist from '../classes/PlayerCaptionsTracklist.js'; import type CardCollection from '../classes/CardCollection.js'; @@ -21,11 +23,19 @@ import type EngagementPanelSectionList from '../classes/EngagementPanelSectionLi import type AppendContinuationItemsAction from '../classes/actions/AppendContinuationItemsAction.js'; import type MusicThumbnail from '../classes/MusicThumbnail.js'; import type OpenPopupAction from '../classes/actions/OpenPopupAction.js'; +import type { ContinuationContents } from '../parser.js'; export interface IParsedResponse { background?: MusicThumbnail; challenge?: string; bg_challenge?: IBotguardChallenge; + botguard_data?: IBotguardData; + ctx?: string; + should_fetch_reauth_session_token?: boolean; + encoded_reauth_proof_token?: string; + session_risk_ctx?: string; + session_token?: string; + eats?: string; actions?: SuperParsedResult; actions_memo?: Memo; content?: YTNode; @@ -45,9 +55,10 @@ export interface IParsedResponse { on_response_received_commands?: ObservedArray; on_response_received_commands_memo?: Memo; continuation?: Continuation; - continuation_contents?: ItemSectionContinuation | SectionListContinuation | LiveChatContinuation | MusicPlaylistShelfContinuation | - MusicShelfContinuation | GridContinuation | PlaylistPanelContinuation | ContinuationCommand; + continuation_contents?: ContinuationContents; continuation_contents_memo?: Memo; + continuation_contents_array?: ContinuationContents[]; + continuation_contents_array_memo?: Memo; metadata?: SuperParsedResult; microformat?: YTNode; overlay?: YTNode; @@ -82,6 +93,11 @@ export interface IParsedResponse { continuation_endpoint?: YTNode; player_response?: IPlayerResponse; watch_next_response?: INextResponse; + video_id?: string; + translation?: Translation; + creator_video?: CreatorVideo; + feedback_responses?: { isProcessed: boolean }[]; + challenge_prompt_type?: 'CHALLENGE_PROMPT_TYPE_UNSPECIFIED' | 'CHALLENGE_PROMPT_TYPE_AUTHENTICATE'; } export interface ITrustedResource { @@ -97,6 +113,11 @@ export interface IBotguardChallenge { client_experiments_state_blob: string; } +export interface IBotguardData { + interpreter_url: ITrustedResource; + program: string; +} + export interface IPlaybackTracking { videostats_watchtime_url: string; videostats_playback_url: string; @@ -148,5 +169,14 @@ export type IGetTranscriptResponse = Pick; export type IUpdatedMetadataResponse = Pick; export type IGuideResponse = Pick; -export type IGetChallengeResponse = Pick; -export type IShowEngagementPanelResponse = Pick; \ No newline at end of file +export type IGetChallengeResponse = Pick; +export type IESRChallengeResponse = Pick; +export type IGetSessionTokenResponse = Pick; +export type IGetWebReauthURLResponse = Pick; +export type IShowEngagementPanelResponse = Pick; +export type ICreateCaptionsResponse = Pick; +export type IParseCaptionsResponse = Pick; +export type IUpdateCaptionsResponse = Pick; +export type IMetadataUpdateResponse = Pick; +export type ICreateVideoResponse = Pick; +export type IUploadFeedbackResponse = Pick; diff --git a/src/parser/types/RawResponse.ts b/src/parser/types/RawResponse.ts index 25d4c7dc6f..861c7ae2a7 100644 --- a/src/parser/types/RawResponse.ts +++ b/src/parser/types/RawResponse.ts @@ -1586,7 +1586,7 @@ export interface IRawResponse { onResponseReceivedActions?: RawNode[]; onResponseReceivedEndpoints?: RawNode[]; onResponseReceivedCommands?: RawNode[]; - continuationContents?: RawNode; + continuationContents?: RawData; actions?: RawNode[]; liveChatItemContextMenuSupportedRenderers?: RawNode; header?: RawNode; @@ -1641,5 +1641,9 @@ export interface IRawResponse { frameworkUpdates?: any; engagementPanels?: RawNode[]; entries?: RawNode[]; + videoId?: string; + translation?: RawNode; + videos?: RawNode[]; + feedbackResponses?: { isProcessed: boolean }[]; [key: string]: any; } diff --git a/src/parser/youtube/History.ts b/src/parser/youtube/History.ts index 5467b72a4b..fb61e18add 100644 --- a/src/parser/youtube/History.ts +++ b/src/parser/youtube/History.ts @@ -65,7 +65,7 @@ export default class History extends Feed { const response = await this.actions.execute('/feedback', body); const data = response.data; - if (!data.feedbackResponses[0].isProcessed) { + if (!data.feedbackResponses?.[0].isProcessed) { throw new Error('Failed to remove video from watch history'); } diff --git a/src/types/BotGuard.ts b/src/types/BotGuard.ts new file mode 100644 index 0000000000..861962d194 --- /dev/null +++ b/src/types/BotGuard.ts @@ -0,0 +1,18 @@ +export interface BotGuardChallenge { + program: string; + global_name: string; + interpreter_hash?: string; + interpreter_url: string; +} +// TODO, make this more comprehensive +export interface AttestationBinding { + // challenge + c: string; + // engagement_type + e?: string; + externalChannelId?: string; + encryptedVideoId?: string; +} +export interface BotGuardSolver { + solve: (botguard_challenge: BotGuardChallenge, binding: T) => Promise; +} diff --git a/src/types/Misc.ts b/src/types/Misc.ts index 128ea49ce1..600b477952 100644 --- a/src/types/Misc.ts +++ b/src/types/Misc.ts @@ -2,7 +2,7 @@ import type { SessionOptions } from '../core/index.js'; export type InnerTubeConfig = SessionOptions; export type InnerTubeClient = 'IOS' | 'WEB' | 'MWEB' | 'ANDROID' | 'ANDROID_VR' | 'VISIONOS' | 'YTMUSIC' | 'YTMUSIC_ANDROID' | 'YTSTUDIO_ANDROID' | 'TV' | 'TV_SIMPLY' |'TV_EMBEDDED' | 'YTKIDS' | 'WEB_EMBEDDED' | 'WEB_CREATOR'; -export type EngagementType = 'ENGAGEMENT_TYPE_UNBOUND' | 'ENGAGEMENT_TYPE_VIDEO_LIKE' | 'ENGAGEMENT_TYPE_VIDEO_DISLIKE' | 'ENGAGEMENT_TYPE_SUBSCRIBE' | 'ENGAGEMENT_TYPE_PLAYBACK' | 'ENGAGEMENT_TYPE_YPC_GET_PREMIUM_PAGE' | 'ENGAGEMENT_TYPE_YPC_GET_DOWNLOAD_ACTION'; +export type EngagementType = 'ENGAGEMENT_TYPE_UNBOUND' | 'ENGAGEMENT_TYPE_VIDEO_LIKE' | 'ENGAGEMENT_TYPE_VIDEO_DISLIKE' | 'ENGAGEMENT_TYPE_SUBSCRIBE' | 'ENGAGEMENT_TYPE_PLAYBACK' | 'ENGAGEMENT_TYPE_YPC_GET_PREMIUM_PAGE' | 'ENGAGEMENT_TYPE_YPC_GET_DOWNLOAD_ACTION' | 'ENGAGEMENT_TYPE_CREATOR_STUDIO_ACTION'; export type UploadDate = 'all' | 'today' | 'week' | 'month' | 'year'; export type SearchType = 'all' | 'video' | 'shorts' | 'channel' | 'playlist' | 'movie'; diff --git a/src/types/StudioWebUploading.ts b/src/types/StudioWebUploading.ts new file mode 100644 index 0000000000..ba8b9489b0 --- /dev/null +++ b/src/types/StudioWebUploading.ts @@ -0,0 +1,67 @@ +export interface BufferReader { + total_bytes: number; + read_chunk: (position: number, length: number) => Promise; +}; +export interface BufferBase64 { + base64: string; +}; + +export interface FileNamedBufferReader { + file_name: string; + source: BufferReader; +}; + +export interface FileNamedBufferBase64 { + file_name: string; + source: BufferBase64; +}; + +export type StudioVisibility = 'PUBLIC' | 'UNLISTED' | 'PRIVATE'; + +export interface UploadVideoDetails { + title?: string; + description?: string; + thumbnail?: FileNamedBufferReader; + playlists?: string[]; + audience?: 'MADE_FOR_KIDS'|'NOT_MADE_FOR_KIDS'; + + paid_promotion?: boolean; + ai_use?: boolean; + + collaboration_channels?: { + id: string; + analytics_setting: 'VIDEO_COLLABORATOR_ANALYTICS_SETTING_NONE'|'VIDEO_COLLABORATOR_ANALYTICS_SETTING_BASIC' + }[]; + + automatic_chapters?: boolean; + featured_places?: boolean; + automatic_concepts?: boolean; + tags?: string[]; + + video_language?: string; + caption_certification?: string; + title_and_description_language?: string; + + recording_date?: Date; + video_location?: string; + + license?: string; + allow_embedding?: boolean; + publish_to_subscriptions_feed_and_notify_subscribers?: boolean; + shorts_remixing?: 'ALLOW_VIDEO_AND_AUDIO'|'ALLOW_ONLY_AUDIO'|'DONT_ALLOW'; + category?: 'FILM'|'AUTOS'|'MUSIC'|'PETS'|'SPORTS'|'TRAVEL'|'GADGETS'|'PEOPLE'|'COMEDY'|'ENTERTAINMENT'|'NEWS'|'HOWTO'|'EDUCATION'|'SCIENCE'|'GOVERNMENT'; + + allow_comments?: 'ON'|'OFF'|'PAUSE'; + comment_moderation?: 'NONE'|'BASIC'|'STRICT'|'HOLD_ALL'; + who_can_comment?: 'ANYONE'|'SUBSCRIBERS_AND_MEMBERS'; + sort_comments_by?: 'TOP'|'NEWEST'; + show_how_many_viewers_like_this_video?: boolean; + + visibility?: StudioVisibility; + subtitles?: { + data: FileNamedBufferBase64; + synced: boolean; + auto_translate?: boolean; + overwrite?: boolean; + }; +}; diff --git a/src/types/index.ts b/src/types/index.ts index ec6da7eeb0..0829b4bfb7 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,7 @@ export type { default as PlatformShim } from './PlatformShim.js'; +export type * from './StudioWebUploading.js'; +export type * from './BotGuard.js'; export type * from './Cache.js'; export type * from './PlatformShim.js'; export type * from './Misc.js'; diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index a61d4ee9e9..d68ffcc37c 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -1,9 +1,11 @@ - export const URLS = { YT_BASE: 'https://www.youtube.com', YT_MUSIC_BASE: 'https://music.youtube.com', + YT_STUDIO_WEB_BASE: 'https://studio.youtube.com', YT_SUGGESTIONS: 'https://suggestqueries-clients6.youtube.com', YT_UPLOAD: 'https://upload.youtube.com/', + YT_UPLOAD_VIDEO_WEB: 'https://upload.youtube.com/upload/studio', + YT_UPLOAD_THUMBNAIL_WEB: 'https://upload.youtube.com/upload/studiothumbnail', API: { BASE: 'https://youtubei.googleapis.com', PRODUCTION_1: 'https://www.youtube.com/youtubei/', @@ -105,10 +107,12 @@ export const CLIENTS = { }, WEB_CREATOR: { NAME: 'WEB_CREATOR', - VERSION: '1.20241203.01.00', + VERSION: '1.20260728.03.00', API_KEY: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8', API_VERSION: 'v1', - STATIC_VISITOR_ID: '6zpwvWUNAco' + STATIC_VISITOR_ID: '6zpwvWUNAco', + USER_AGENT: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36', + EATS: 'AeCS5zA8mwKJA3VzvwD--o2-ZsGbWYFt6LMN2EuOPJLQrg6MIuKxBpbf9WNlMPlARBhbMM-hSWg982LQDZEnOBj-yHFw1TDTzIUdINTExUA6U5lOgLtxyv6guJS9HQ==' } } as const; /** diff --git a/src/utils/HTTPClient.ts b/src/utils/HTTPClient.ts index 18aff55120..f81dcecec5 100644 --- a/src/utils/HTTPClient.ts +++ b/src/utils/HTTPClient.ts @@ -333,6 +333,7 @@ export default class HTTPClient { case 'WEB_CREATOR': ctx.client.clientName = Constants.CLIENTS.WEB_CREATOR.NAME; ctx.client.clientVersion = Constants.CLIENTS.WEB_CREATOR.VERSION; + if (ctx.request) ctx.request.returnLogEntry = true; break; default: break; diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index afe61e2378..bddeab809e 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -299,4 +299,10 @@ export function getNsigProcessorFn(n?: string | null, sp?: string | null, s?: st } return process("${n || ''}", "${sp || ''}", "${s || ''}");`; +} + +export async function wait(milliseconds: number) { + return new Promise(function (resolve) { + setTimeout(resolve, milliseconds); + }); } \ No newline at end of file