mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-26 16:18:51 +00:00
* refactor: remove dependancies removes node-forge and uuid in favor of Web APIs * refactor!: commonjs to es6 To aid with #93 I will make all my changes in TypeScript instead. This is the first step into making that happen. Used: https://github.com/wessberg/cjstoesm * refactor!: NToken and Signature TS files Bring this PR up to speed with #93 * feat: cross platform cache (WIP) this is untested! should remove idb as dependecy. * feat: EventEmitter polyfill * refactor: remove events * feat: HTTPClient based on Fetch API (WIP) * refactor!: parsers refactor (WIP) Initial TS support for parsers as per #93 This adds several type safety checks to the parser which'll help to ensure valid data is returned by the parser. * refactor!: parsers refactor (WIP) Bring more in line with the existing implementations & make less verbose * refactor!: parser refactor I was overcomplicating things, this is much simpler and compatible with the existing JS API * fix: some missed parsers while refactoring * fix: better type inferance for parseResponse * feat(TS): typesafe YTNode casts * feat: more type safety in YTNode and Parser * refactor: VideoInfo download with fetch & TS (WIP) Again, this also does some work for #93 * fix: LiveChat in VideoInfo * refactor!: more typesafety in parser * refactor!: VideoInfo almost completed * refactor!: player and session refactors - Remove the Player class' dependance on Session. - Add additional context to the Session. * refactor!: move auth logic to Session (WIP) * refactor: TS port for Actions and Innertube My fingers hurt from typing out all those types :-P * refactor: NavigationEndpoint TS this is still a WIP and should be improved. NavigationEndpoint should probably be refactored further. * refactor!: VideoInfo compiles without errors * chore: delete old player * fix: import errors It compiles and runs!! * fix: Utils import fixes * fix: several runtime errors * fix: video streaming * chore: remove console.log debugging Whoops, forgot to remove these before I pushed the previous commit * chore: remove old unused dependencies * fix: typescript errors Now emitting declarations and source maps * refactor: TS feed * chore: delete old Feed * refactor: move streamToIterable into Utils * refactor: AccountManager TS * refactor: FilterableFeed to TS * refactor: InteractionManager to TS * refactor: PlaylistManager to TS * refactor: TabbedFeed to TS * refactor: Music to TS (WIP) more work to be done, see TODO comments * fix: getting the tests to pass (6/12) YouTube.js Tests Search ✓ Should search on YouTube (1152 ms) ✕ Should search on YouTube Music (705 ms) ✕ Should retrieve YouTube search suggestions (722 ms) ✓ Should retrieve YouTube Music search suggestions (233 ms) Comments ✓ Should retrieve comments (585 ms) ✕ Should retrieve next batch of comments (221 ms) ✕ Should retrieve comment replies (1 ms) General ✕ Should retrieve playlist with YouTube (732 ms) ✓ Should retrieve home feed (838 ms) ✓ Should retrieve trending content (543 ms) ✓ Should retrieve video info (639 ms) ✕ Should download video (5 ms) * fix: tests (7/12) YouTube.js Tests Search ✓ Should search on YouTube (1984 ms) ✕ Should search on YouTube Music (1139 ms) ✕ Should retrieve YouTube search suggestions (1433 ms) ✓ Should retrieve YouTube Music search suggestions (529 ms) Comments ✓ Should retrieve comments (324 ms) ✓ Should retrieve next batch of comments (395 ms) ✕ Should retrieve comment replies General ✕ Should retrieve playlist with YouTube (653 ms) ✓ Should retrieve home feed (1085 ms) ✓ Should retrieve trending content (513 ms) ✓ Should retrieve video info (921 ms) ✕ Should download video (3 ms) * fix: download tests (8/12) YouTube.js Tests Search ✓ Should search on YouTube (1293 ms) ✕ Should search on YouTube Music (927 ms) ✕ Should retrieve YouTube search suggestions (1250 ms) ✓ Should retrieve YouTube Music search suggestions (258 ms) Comments ✓ Should retrieve comments (803 ms) ✓ Should retrieve next batch of comments (511 ms) ✕ Should retrieve comment replies General ✕ Should retrieve playlist with YouTube (528 ms) ✓ Should retrieve home feed (1047 ms) ✓ Should retrieve trending content (548 ms) ✓ Should retrieve video info (825 ms) ✓ Should download video (1779 ms) * fix: tests (9/12) YouTube.js Tests Search ✓ Should search on YouTube (1276 ms) ✕ Should search on YouTube Music (955 ms) ✓ Should retrieve YouTube search suggestions (661 ms) ✓ Should retrieve YouTube Music search suggestions (491 ms) Comments ✓ Should retrieve comments (624 ms) ✓ Should retrieve next batch of comments (353 ms) ✕ Should retrieve comment replies General ✕ Should retrieve playlist with YouTube (672 ms) ✓ Should retrieve home feed (1277 ms) ✓ Should retrieve trending content (999 ms) ✓ Should retrieve video info (1106 ms) ✓ Should download video (2514 ms) * feat: key based type validation for parsers * fix: comments tests pass (10/12) YouTube.js Tests Search ✓ Should search on YouTube (938 ms) ✕ Should search on YouTube Music (850 ms) ✓ Should retrieve YouTube search suggestions (528 ms) ✓ Should retrieve YouTube Music search suggestions (224 ms) Comments ✓ Should retrieve comments (518 ms) ✓ Should retrieve next batch of comments (337 ms) ✓ Should retrieve comment replies (358 ms) General ✕ Should retrieve playlist with YouTube (466 ms) ✓ Should retrieve home feed (1051 ms) ✓ Should retrieve trending content (623 ms) ✓ Should retrieve video info (863 ms) ✓ Should download video (2656 ms) * refactor: type safety checks removing @ts-ignore * fix: playlist tests pass (11/12) YouTube.js Tests Search ✓ Should search on YouTube (991 ms) ✕ Should search on YouTube Music (924 ms) ✓ Should retrieve YouTube search suggestions (606 ms) ✓ Should retrieve YouTube Music search suggestions (225 ms) Comments ✓ Should retrieve comments (393 ms) ✓ Should retrieve next batch of comments (284 ms) ✓ Should retrieve comment replies (252 ms) General ✓ Should retrieve playlist with YouTube (578 ms) ✓ Should retrieve home feed (1148 ms) ✓ Should retrieve trending content (541 ms) ✓ Should retrieve video info (799 ms) ✓ Should download video (1419 ms) * fix: all tests pass for node 🎉 YouTube.js Tests Search ✓ Should search on YouTube (1053 ms) ✓ Should search on YouTube Music (761 ms) ✓ Should retrieve YouTube search suggestions (453 ms) ✓ Should retrieve YouTube Music search suggestions (221 ms) Comments ✓ Should retrieve comments (627 ms) ✓ Should retrieve next batch of comments (412 ms) ✓ Should retrieve comment replies (268 ms) General ✓ Should retrieve playlist with YouTube (565 ms) ✓ Should retrieve home feed (775 ms) ✓ Should retrieve trending content (498 ms) ✓ Should retrieve video info (875 ms) ✓ Should download video (1364 ms) * build: working Deno bundle Still need to test whether this bundle works in the browser * docs: update deno example to download video * refactor: MusicResponsiveListItem to TS * docs: TSDoc for Parser helpers * docs: Parser documentation for TS * docs: add note about parseItem and parseArray * test: remove browser tests since they're identical * feat: browser support and proxy example * fix: PlaylistManager TS after merge * feat: in-browser video streaming * refactor: cleanup the Dash example * feat: allow custom fetch implementations * feat: fetch debugger * fix: OAuth login * refactor: remove file extensions from imports * refactor: build scripts * fix: CustomEvent on node * fix: LiveChat * fix: linting * fix: liniting in build-parser-json * chore: update test workflow * fix: NToken errors after lint fixes * fix: codacy complaints * docs: update to reflect changes Definitly needs more work but its a start * refactor: cleanup imports/exports * fix: browser example - Remove user-agent before making request. - Fix cache on browsers * fix: cache on node * fix: stupid mistake * refactor: Session#signIn to wait untill success This also splits the 'auth' event up into 3 distinct events: - 'auth' -> fired on success - 'auth-pending' -> fired when pending authentication - 'auth-error' -> fired when an error occurred * refactor: freeze Constants * refactor: cleanup HTTPClient Request * refactor: debugFetch readability * chore: lint * refactor: replace jsdoc with tsdoc eslint plugin remove @param annotations without descriptions * fix: bunch of liniting warnings * refactor: better inference on YTNode#is As suggested by @MasterOfBob777 * fix: linting warnings * revert: undici import * refactor: rename `list_type` to `item_type`
700 lines
20 KiB
TypeScript
700 lines
20 KiB
TypeScript
import Proto from '../proto/index';
|
|
import { hasKeys, InnertubeError, MissingParamError, uuidv4 } from '../utils/Utils';
|
|
import Constants from '../utils/Constants';
|
|
import Parser, { ParsedResponse } from '../parser/index';
|
|
import Session from './Session';
|
|
|
|
|
|
export interface BrowseArgs {
|
|
params?: string;
|
|
is_ytm?: boolean;
|
|
is_ctoken?: boolean;
|
|
client?: string;
|
|
}
|
|
|
|
export interface EngageArgs {
|
|
video_id?: string;
|
|
channel_id?: string;
|
|
comment_id?: string;
|
|
comment_action?: string;
|
|
params?: string;
|
|
text?: string;
|
|
target_language?: string;
|
|
}
|
|
|
|
export interface AccountArgs {
|
|
new_value?: string | boolean; // TODO: is this correct?
|
|
setting_item_id?: string;
|
|
client?: string;
|
|
}
|
|
|
|
export interface SearchArgs {
|
|
query?: string,
|
|
options?: {
|
|
period?: string,
|
|
duration?: string,
|
|
order?: string
|
|
},
|
|
client?: string,
|
|
ctoken?: string,
|
|
params?: string
|
|
filters?: any // TODO: what is this type??
|
|
}
|
|
|
|
export interface AxioslikeResponse {
|
|
success: boolean;
|
|
status_code: number;
|
|
data: any;
|
|
}
|
|
|
|
export type ActionsResponse = Promise<AxioslikeResponse>;
|
|
|
|
class Actions {
|
|
#session;
|
|
constructor(session: Session) {
|
|
this.#session = session;
|
|
}
|
|
get session() {
|
|
return this.#session;
|
|
}
|
|
|
|
/**
|
|
* Mimmics the Axios API using Fetch's Response object.
|
|
*/
|
|
async #wrap(response: Response) {
|
|
return {
|
|
success: response.ok,
|
|
status_code: response.status,
|
|
data: await response.json()
|
|
};
|
|
}
|
|
/**
|
|
* Covers `/browse` endpoint, mostly used to access
|
|
* YouTube's sections such as the home feed, etc
|
|
* and sometimes to retrieve continuations.
|
|
*
|
|
* @param id - browseId or a continuation token
|
|
* @param args - additional arguments
|
|
*/
|
|
async browse(id: string, args: BrowseArgs = {}) {
|
|
if (this.#needsLogin(id) && !this.#session.logged_in)
|
|
throw new InnertubeError('You are not signed in');
|
|
const data: Record<string, any> = {};
|
|
if (args.params)
|
|
data.params = args.params;
|
|
if (args.is_ctoken) {
|
|
data.continuation = id;
|
|
} else {
|
|
data.browseId = id;
|
|
}
|
|
if (args.client) {
|
|
data.client = args.client;
|
|
}
|
|
const response = await this.#session.http.fetch('/browse', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
return this.#wrap(response);
|
|
}
|
|
/**
|
|
* Covers endpoints used to perform direct interactions
|
|
* on YouTube.
|
|
*/
|
|
async engage(action: string, args: EngageArgs = {}) {
|
|
if (!this.#session.logged_in && !args.hasOwnProperty('text'))
|
|
throw new InnertubeError('You are not signed in');
|
|
const data: Record<string, any> = {};
|
|
switch (action) {
|
|
case 'like/like':
|
|
case 'like/dislike':
|
|
case 'like/removelike':
|
|
if (!hasKeys(args, 'video_id'))
|
|
throw new MissingParamError('Arguments lacks video_id');
|
|
data.target = {};
|
|
data.target.videoId = args.video_id;
|
|
if (args.params) {
|
|
data.params = args.params;
|
|
}
|
|
break;
|
|
case 'subscription/subscribe':
|
|
case 'subscription/unsubscribe':
|
|
if (!hasKeys(args, 'channel_id'))
|
|
throw new MissingParamError('Arguments lacks channel_id');
|
|
data.channelIds = [ args.channel_id ];
|
|
data.params = action === 'subscription/subscribe' ? 'EgIIAhgA' : 'CgIIAhgA';
|
|
break;
|
|
case 'comment/create_comment':
|
|
data.commentText = args.text;
|
|
if (!hasKeys(args, 'video_id'))
|
|
throw new MissingParamError('Arguments lacks video_id');
|
|
data.createCommentParams = Proto.encodeCommentParams(args.video_id);
|
|
break;
|
|
case 'comment/create_comment_reply':
|
|
if (!hasKeys(args, 'comment_id', 'video_id', 'text'))
|
|
throw new MissingParamError('Arguments lacks comment_id, video_id or text');
|
|
data.createReplyParams = Proto.encodeCommentReplyParams(args.comment_id, args.video_id);
|
|
data.commentText = args.text;
|
|
break;
|
|
case 'comment/perform_comment_action':
|
|
const target_action = (() => {
|
|
switch (args.comment_action) {
|
|
case 'like':
|
|
return Proto.encodeCommentActionParams(5, args);
|
|
case 'dislike':
|
|
return Proto.encodeCommentActionParams(4, args);
|
|
case 'translate':
|
|
return Proto.encodeCommentActionParams(22, args);
|
|
default:
|
|
break;
|
|
}
|
|
})();
|
|
data.actions = [ target_action ];
|
|
break;
|
|
default:
|
|
throw new InnertubeError('Action not implemented', action);
|
|
}
|
|
const response = await this.#session.http.fetch(`/${action}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
return this.#wrap(response);
|
|
}
|
|
/**
|
|
* Covers endpoints related to account management.
|
|
*/
|
|
async account(action: string, args: AccountArgs = {}) {
|
|
if (!this.#session.logged_in)
|
|
throw new InnertubeError('You are not signed in');
|
|
const data: Record<string, any> = {
|
|
client: args.client
|
|
};
|
|
switch (action) {
|
|
case 'account/set_setting':
|
|
data.newValue = {
|
|
boolValue: args.new_value
|
|
};
|
|
data.settingItemId = args.setting_item_id;
|
|
break;
|
|
case 'account/accounts_list':
|
|
break;
|
|
default:
|
|
throw new InnertubeError('Action not implemented', action);
|
|
}
|
|
const response = await this.#session.http.fetch(`/${action}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
return this.#wrap(response);
|
|
}
|
|
/**
|
|
* Endpoint used for search.
|
|
*/
|
|
async search(args: SearchArgs = {}) {
|
|
const data: Record<string, any> = { client: args.client };
|
|
if (args.query) {
|
|
data.query = args.query;
|
|
}
|
|
if (args.ctoken) {
|
|
data.continuation = args.ctoken;
|
|
}
|
|
if (args.params) {
|
|
data.params = args.params;
|
|
}
|
|
if (args.filters) {
|
|
if (args.client == 'YTMUSIC') {
|
|
data.params = Proto.encodeMusicSearchFilters(args.filters);
|
|
} else {
|
|
data.params = Proto.encodeSearchFilters(args.filters);
|
|
}
|
|
}
|
|
const response = await this.#session.http.fetch('/search', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
return this.#wrap(response);
|
|
}
|
|
/**
|
|
* Endpoint used fo Shorts' sound search.
|
|
*/
|
|
async searchSound(args: {
|
|
query: string;
|
|
}) {
|
|
const data = {
|
|
query: args.query,
|
|
client: 'ANDROID'
|
|
};
|
|
const response = await this.#session.http.fetch('/sfv/search', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
return this.#wrap(response);
|
|
}
|
|
/**
|
|
* Channel management endpoints.
|
|
*
|
|
*/
|
|
async channel(action: string, args: {
|
|
new_name?: string;
|
|
new_description?: string;
|
|
client?: string;
|
|
} = {}) {
|
|
if (!this.#session.logged_in)
|
|
throw new InnertubeError('You are not signed in');
|
|
const data: Record<string, any> = {
|
|
client: args.client || 'ANDROID'
|
|
};
|
|
switch (action) {
|
|
case 'channel/edit_name':
|
|
data.givenName = args.new_name;
|
|
break;
|
|
case 'channel/edit_description':
|
|
data.description = args.new_description;
|
|
break;
|
|
case 'channel/get_profile_editor':
|
|
break;
|
|
default:
|
|
throw new InnertubeError('Action not implemented', action);
|
|
}
|
|
const response = await this.#session.http.fetch(`/${action}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
return this.#wrap(response);
|
|
}
|
|
/**
|
|
* Covers endpoints used for playlist management.
|
|
*
|
|
*/
|
|
async playlist(action: string, args: {
|
|
title?: string;
|
|
ids?: string[]; // TODO: this was a string before, but I made it an array, is this correct?
|
|
playlist_id?: string;
|
|
action?: string;
|
|
} = {}) {
|
|
if (!this.#session.logged_in)
|
|
throw new InnertubeError('You are not signed in');
|
|
const data: Record<string, any> = {};
|
|
switch (action) {
|
|
case 'playlist/create':
|
|
data.title = args.title;
|
|
data.videoIds = args.ids;
|
|
break;
|
|
case 'playlist/delete':
|
|
data.playlistId = args.playlist_id;
|
|
break;
|
|
case 'browse/edit_playlist':
|
|
if (!hasKeys(args, 'ids'))
|
|
throw new MissingParamError('Arguments lacks ids');
|
|
data.playlistId = args.playlist_id;
|
|
data.actions = args.ids.map((id) => {
|
|
switch (args.action) {
|
|
case 'ACTION_ADD_VIDEO':
|
|
return {
|
|
action: args.action,
|
|
addedVideoId: id
|
|
};
|
|
case 'ACTION_REMOVE_VIDEO':
|
|
return {
|
|
action: args.action,
|
|
setVideoId: id
|
|
};
|
|
default:
|
|
break;
|
|
}
|
|
});
|
|
break;
|
|
default:
|
|
throw new InnertubeError('Action not implemented', action);
|
|
}
|
|
const response = await this.#session.http.fetch(`/${action}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
return this.#wrap(response);
|
|
}
|
|
/**
|
|
* Covers endpoints used for notifications management.
|
|
*/
|
|
async notifications(action: string, args: {
|
|
pref?: string;
|
|
channel_id?: string;
|
|
ctoken?: string;
|
|
params?: string
|
|
} = {}) {
|
|
if (!this.#session.logged_in)
|
|
throw new InnertubeError('You are not signed in');
|
|
const data: Record<string, any> = {};
|
|
switch (action) {
|
|
case 'modify_channel_preference':
|
|
if (!hasKeys(args, 'channel_id', 'pref'))
|
|
throw new MissingParamError('Arguments lacks channel_id or pref');
|
|
const pref_types = {
|
|
PERSONALIZED: 1,
|
|
ALL: 2,
|
|
NONE: 3
|
|
};
|
|
if (!Object.keys(pref_types).includes(args.pref.toUpperCase()))
|
|
throw new InnertubeError('Invalid preference type', args.pref);
|
|
data.params = Proto.encodeNotificationPref(args.channel_id, pref_types[args.pref.toUpperCase() as keyof typeof pref_types]);
|
|
break;
|
|
case 'get_notification_menu':
|
|
data.notificationsMenuRequestType = 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX';
|
|
if (args.ctoken)
|
|
data.ctoken = args.ctoken;
|
|
break;
|
|
case 'record_interactions':
|
|
data.serializedRecordNotificationInteractionsRequest = args.params;
|
|
break;
|
|
case 'get_unseen_count':
|
|
break;
|
|
default:
|
|
throw new InnertubeError('Action not implemented', action);
|
|
}
|
|
const response = await this.#session.http.fetch(`/notification/${action}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
return this.#wrap(response);
|
|
}
|
|
/**
|
|
* Covers livechat endpoints.
|
|
*/
|
|
async livechat(action: string, args: {
|
|
text?: string;
|
|
video_id?: string;
|
|
channel_id?: string;
|
|
ctoken?: string;
|
|
params?: string;
|
|
client?: string;
|
|
} = {}) {
|
|
// TODO: should client be required?
|
|
const data: Record<string, any> = { client: args.client };
|
|
switch (action) {
|
|
case 'live_chat/get_live_chat':
|
|
case 'live_chat/get_live_chat_replay':
|
|
data.continuation = args.ctoken;
|
|
break;
|
|
case 'live_chat/send_message':
|
|
if (!hasKeys(args, 'channel_id', 'video_id', 'text'))
|
|
throw new MissingParamError('Arguments lacks channel_id, video_id or text');
|
|
data.params = Proto.encodeMessageParams(args.channel_id, args.video_id);
|
|
data.clientMessageId = uuidv4();
|
|
data.richMessage = {
|
|
textSegments: [ {
|
|
text: args.text
|
|
} ]
|
|
};
|
|
break;
|
|
case 'live_chat/get_item_context_menu':
|
|
// Note: this is currently broken due to a recent refactor
|
|
// TODO: this should be implemented
|
|
break;
|
|
case 'live_chat/moderate':
|
|
data.params = args.params;
|
|
break;
|
|
case 'updated_metadata':
|
|
data.videoId = args.video_id;
|
|
if (args.ctoken)
|
|
data.continuation = args.ctoken;
|
|
break;
|
|
default:
|
|
throw new InnertubeError('Action not implemented', action);
|
|
}
|
|
const response = await this.#session.http.fetch(`/${action}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
return this.#wrap(response);
|
|
}
|
|
/**
|
|
* Endpoint used to retrieve video thumbnails.
|
|
*/
|
|
async thumbnails(args: {
|
|
video_id: string;
|
|
}) {
|
|
const data = {
|
|
client: 'ANDROID',
|
|
videoId: args.video_id
|
|
};
|
|
const response = await this.#session.http.fetch('/thumbnails', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
return this.#wrap(response);
|
|
}
|
|
/**
|
|
* Place Autocomplete endpoint, found it in the APK but
|
|
* doesn't seem to be used anywhere on YouTube (maybe for ads?).
|
|
*
|
|
* Ex:
|
|
* ```js
|
|
* const places = await session.actions.geo('place_autocomplete', { input: 'San diego cafe' });
|
|
* console.info(places.data);
|
|
* ```
|
|
*/
|
|
async geo(action: string, args: {
|
|
input: string;
|
|
}) {
|
|
if (!this.#session.logged_in)
|
|
throw new InnertubeError('You are not signed in');
|
|
const data = {
|
|
input: args.input,
|
|
client: 'ANDROID'
|
|
};
|
|
const response = await this.#session.http.fetch(`/geo/${action}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
return this.#wrap(response);
|
|
}
|
|
/**
|
|
* Covers endpoints used to report content.
|
|
*/
|
|
async flag(action: string, args: {
|
|
action: string;
|
|
params?: string;
|
|
}) {
|
|
if (!this.#session.logged_in)
|
|
throw new InnertubeError('You are not signed in');
|
|
const data: Record<string, any> = {};
|
|
switch (action) {
|
|
case 'flag/flag':
|
|
data.action = args.action;
|
|
break;
|
|
case 'flag/get_form':
|
|
data.params = args.params;
|
|
break;
|
|
default:
|
|
throw new InnertubeError('Action not implemented', action);
|
|
}
|
|
const response = await this.#session.http.fetch(`/${action}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
return this.#wrap(response);
|
|
}
|
|
/**
|
|
* Covers specific YouTube Music endpoints.
|
|
*/
|
|
async music(action: string, args: {
|
|
input?: string;
|
|
}) {
|
|
const data = {
|
|
input: args.input || '',
|
|
client: 'YTMUSIC'
|
|
};
|
|
const response = await this.#session.http.fetch(`/music/${action}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
return this.#wrap(response);
|
|
}
|
|
/**
|
|
* Mostly used for pagination and specific operations.
|
|
*/
|
|
async next(args: {
|
|
video_id?: string;
|
|
ctoken?: string;
|
|
client?: string;
|
|
} = {}) {
|
|
const data: Record<string, any> = { client: args.client };
|
|
if (args.ctoken) {
|
|
data.continuation = args.ctoken;
|
|
}
|
|
if (args.video_id) {
|
|
data.videoId = args.video_id;
|
|
}
|
|
const response = await this.#session.http.fetch('/next', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
return this.#wrap(response);
|
|
}
|
|
/**
|
|
* Used to retrieve video info.
|
|
*/
|
|
async getVideoInfo(id: string, cpn?: string, client?: string) {
|
|
const data: Record<string, any> = {
|
|
playbackContext: {
|
|
contentPlaybackContext: {
|
|
vis: 0,
|
|
splay: false,
|
|
referer: 'https://www.youtube.com',
|
|
currentUrl: `/watch?v=${id}`,
|
|
autonavState: 'STATE_OFF',
|
|
signatureTimestamp: this.#session.player.sts,
|
|
autoCaptionsDefaultOn: false,
|
|
html5Preference: 'HTML5_PREF_WANTS',
|
|
lactMilliseconds: '-1'
|
|
}
|
|
},
|
|
attestationRequest: {
|
|
omitBotguardData: true
|
|
},
|
|
videoId: id
|
|
};
|
|
if (client) {
|
|
data.client = client;
|
|
}
|
|
if (cpn) {
|
|
data.cpn = cpn;
|
|
}
|
|
const response = await this.#session.http.fetch('/player', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
return this.#wrap(response);
|
|
}
|
|
/**
|
|
* Covers search suggestion endpoints.
|
|
*/
|
|
async getSearchSuggestions(client: 'YOUTUBE' | 'YTMUSIC', query: string) {
|
|
if (![ 'YOUTUBE', 'YTMUSIC' ].includes(client))
|
|
throw new InnertubeError('Invalid client', client);
|
|
const response = await ({
|
|
YOUTUBE: async () => {
|
|
const params = new URLSearchParams({
|
|
q: query,
|
|
ds: 'yt',
|
|
client: 'youtube',
|
|
xssi: 't',
|
|
oe: 'UTF',
|
|
gl: this.#session.context.client.gl,
|
|
hl: this.#session.context.client.hl
|
|
});
|
|
const response = await this.#session.http.fetch(`search?${params.toString()}`, {
|
|
baseURL: Constants.URLS.YT_SUGGESTIONS,
|
|
method: 'GET'
|
|
});
|
|
return this.#wrap(response);
|
|
},
|
|
YTMUSIC: () => this.music('get_search_suggestions', {
|
|
input: query
|
|
})
|
|
}[client])();
|
|
return response;
|
|
}
|
|
/**
|
|
* Endpoint used to retrieve user mention suggestions.
|
|
*/
|
|
async getUserMentionSuggestions(args: {
|
|
input: string;
|
|
}) {
|
|
if (!this.#session.logged_in)
|
|
throw new InnertubeError('You are not signed in');
|
|
const data = {
|
|
input: args.input,
|
|
client: 'ANDROID'
|
|
};
|
|
const response = await this.#session.http.fetch('/get_user_mention_suggestions', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
return this.#wrap(response);
|
|
}
|
|
/**
|
|
* Executes an API call.
|
|
* @param action - endpoint
|
|
* @param args - call arguments
|
|
*/
|
|
async execute(action: string, args: {
|
|
[key: string]: any;
|
|
parse: true;
|
|
}) : Promise<ParsedResponse>;
|
|
async execute(action: string, args: {
|
|
[key: string]: any;
|
|
parse?: false;
|
|
}) : Promise<ActionsResponse>;
|
|
async execute(action: string, args: {
|
|
[key: string]: any;
|
|
parse?: boolean;
|
|
}): Promise<ParsedResponse | ActionsResponse> {
|
|
const data = { ...args };
|
|
if (Reflect.has(data, 'parse'))
|
|
delete data.parse;
|
|
if (Reflect.has(data, 'request'))
|
|
delete data.request;
|
|
if (Reflect.has(data, 'clientActions'))
|
|
delete data.clientActions;
|
|
if (Reflect.has(data, 'action')) {
|
|
data.actions = [ data.action ];
|
|
delete data.action;
|
|
}
|
|
if (Reflect.has(data, 'token')) {
|
|
data.continuation = data.token;
|
|
delete data.token;
|
|
}
|
|
const response = await this.#session.http.fetch(action, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
if (args.parse) {
|
|
return Parser.parseResponse(await response.json());
|
|
}
|
|
return this.#wrap(response);
|
|
}
|
|
#needsLogin(id: string) {
|
|
return [
|
|
'FElibrary',
|
|
'FEhistory',
|
|
'FEsubscriptions',
|
|
'SPaccount_notifications',
|
|
'SPaccount_privacy',
|
|
'SPtime_watched'
|
|
].includes(id);
|
|
}
|
|
}
|
|
// TODO: maybe do this inferrance in a more elegant way
|
|
export default Actions;
|