@imput/youtubei.js
Version:
A JavaScript client for YouTube's private API, known as InnerTube. Fork of youtubei.js
486 lines • 21.1 kB
JavaScript
var _Innertube_session;
import { __classPrivateFieldGet, __classPrivateFieldSet } from "tslib";
import Session from './core/Session.js';
import { Kids, Music, Studio } from './core/clients/index.js';
import { AccountManager, InteractionManager, PlaylistManager } from './core/managers/index.js';
import { Feed, TabbedFeed } from './core/mixins/index.js';
import { Channel, Comments, Guide, HashtagFeed, History, HomeFeed, Library, NotificationsMenu, Playlist, Search, VideoInfo } from './parser/youtube/index.js';
import { ShortFormVideoInfo } from './parser/ytshorts/index.js';
import NavigationEndpoint from './parser/classes/NavigationEndpoint.js';
import * as Constants from './utils/Constants.js';
import { generateRandomString, InnertubeError, throwIfMissing, u8ToBase64 } from './utils/Utils.js';
import { CommunityPostCommentsParam, CommunityPostCommentsParamContainer, CommunityPostParams, GetCommentsSectionParams, Hashtag, ReelSequence, SearchFilter, SearchFilter_Filters_Duration, SearchFilter_Filters_SearchType, SearchFilter_Filters_UploadDate, SearchFilter_SortBy } from '../protos/generated/misc/params.js';
/**
* Provides access to various services and modules in the YouTube API.
*
* @example
* ```ts
* import { Innertube, UniversalCache } from 'youtubei.js';
* const innertube = await Innertube.create({ cache: new UniversalCache(true)});
* ```
*/
class Innertube {
constructor(session) {
_Innertube_session.set(this, void 0);
__classPrivateFieldSet(this, _Innertube_session, session, "f");
}
static async create(config = {}) {
return new Innertube(await Session.create(config));
}
async getInfo(target, client) {
throwIfMissing({ target });
const payload = {
videoId: target instanceof NavigationEndpoint ? target.payload?.videoId : target,
playlistId: target instanceof NavigationEndpoint ? target.payload?.playlistId : undefined,
playlistIndex: target instanceof NavigationEndpoint ? target.payload?.playlistIndex : undefined,
params: target instanceof NavigationEndpoint ? target.payload?.params : undefined,
racyCheckOk: true,
contentCheckOk: true
};
const watch_endpoint = new NavigationEndpoint({ watchEndpoint: payload });
const watch_next_endpoint = new NavigationEndpoint({ watchNextEndpoint: payload });
const session = __classPrivateFieldGet(this, _Innertube_session, "f");
const extra_payload = {
playbackContext: {
contentPlaybackContext: {
vis: 0,
splay: false,
lactMilliseconds: '-1',
signatureTimestamp: session.player?.sts
}
},
client
};
if (session.po_token) {
extra_payload.serviceIntegrityDimensions = {
poToken: session.po_token
};
}
const watch_response = watch_endpoint.call(session.actions, extra_payload);
const watch_next_response = watch_next_endpoint.call(session.actions);
const response = await Promise.all([watch_response, watch_next_response]);
const cpn = generateRandomString(16);
return new VideoInfo(response, session.actions, cpn);
}
async getBasicInfo(video_id, client) {
throwIfMissing({ video_id });
const watch_endpoint = new NavigationEndpoint({
watchEndpoint: {
videoId: video_id,
racyCheckOk: true,
contentCheckOk: true
}
});
const session = __classPrivateFieldGet(this, _Innertube_session, "f");
const extra_payload = {
playbackContext: {
contentPlaybackContext: {
vis: 0,
splay: false,
lactMilliseconds: '-1',
signatureTimestamp: session.player?.sts
}
},
client
};
if (session.po_token) {
extra_payload.serviceIntegrityDimensions = {
poToken: session.po_token
};
}
const watch_response = await watch_endpoint.call(session.actions, extra_payload);
const cpn = generateRandomString(16);
return new VideoInfo([watch_response], session.actions, cpn);
}
async getShortsVideoInfo(video_id, client) {
throwIfMissing({ video_id });
const reel_watch_endpoint = new NavigationEndpoint({
reelWatchEndpoint: {
disablePlayerResponse: false,
params: 'CAUwAg%3D%3D',
videoId: video_id
}
});
const actions = __classPrivateFieldGet(this, _Innertube_session, "f").actions;
const reel_watch_response = reel_watch_endpoint.call(actions, { client });
const writer = ReelSequence.encode({
shortId: video_id,
params: {
number: 5
},
feature2: 25,
feature3: 0
});
const params = encodeURIComponent(u8ToBase64(writer.finish()));
const sequence_response = actions.execute('/reel/reel_watch_sequence', { sequenceParams: params });
const response = await Promise.all([reel_watch_response, sequence_response]);
const cpn = generateRandomString(16);
return new ShortFormVideoInfo([response[0]], actions, cpn, response[1]);
}
async search(query, filters = {}) {
throwIfMissing({ query });
const search_filter = {};
search_filter.filters = {};
if (filters.sort_by) {
search_filter.sortBy = SearchFilter_SortBy[filters.sort_by.toUpperCase()];
}
if (filters.upload_date) {
search_filter.filters.uploadDate = SearchFilter_Filters_UploadDate[filters.upload_date.toUpperCase()];
}
if (filters.type) {
search_filter.filters.type = SearchFilter_Filters_SearchType[filters.type.toUpperCase()];
}
if (filters.duration) {
search_filter.filters.duration = SearchFilter_Filters_Duration[filters.duration.toUpperCase()];
}
if (filters.features) {
for (const feature of filters.features) {
switch (feature) {
case '360':
search_filter.filters.features360 = true;
break;
case '3d':
search_filter.filters.features3d = true;
break;
case '4k':
search_filter.filters.features4k = true;
break;
case 'creative_commons':
search_filter.filters.featuresCreativeCommons = true;
break;
case 'hd':
search_filter.filters.featuresHd = true;
break;
case 'hdr':
search_filter.filters.featuresHdr = true;
break;
case 'live':
search_filter.filters.featuresLive = true;
break;
case 'location':
search_filter.filters.featuresLocation = true;
break;
case 'purchased':
search_filter.filters.featuresPurchased = true;
break;
case 'subtitles':
search_filter.filters.featuresSubtitles = true;
break;
case 'vr180':
search_filter.filters.featuresVr180 = true;
break;
default:
break;
}
}
}
const search_endpoint = new NavigationEndpoint({
searchEndpoint: {
query,
params: filters ? encodeURIComponent(u8ToBase64(SearchFilter.encode(search_filter).finish())) : undefined
}
});
const response = await search_endpoint.call(__classPrivateFieldGet(this, _Innertube_session, "f").actions);
return new Search(this.actions, response);
}
async getSearchSuggestions(query, previous_query) {
const session = __classPrivateFieldGet(this, _Innertube_session, "f");
const url = new URL(`${Constants.URLS.YT_SUGGESTIONS}/complete/search`);
url.searchParams.set('client', 'youtube');
url.searchParams.set('gs_ri', 'youtube');
url.searchParams.set('gs_id', '0');
url.searchParams.set('cp', '0');
url.searchParams.set('ds', 'yt');
url.searchParams.set('sugexp', Constants.CLIENTS.WEB.SUGG_EXP_ID);
url.searchParams.set('hl', session.context.client.hl);
url.searchParams.set('gl', session.context.client.gl);
url.searchParams.set('q', query);
if (previous_query)
url.searchParams.set('pq', previous_query);
const response = await session.http.fetch_function(url, {
headers: {
'Cookie': session.cookie || ''
}
});
const text = await response.text();
const data = JSON.parse(text.replace('window.google.ac.h(', '').slice(0, -1));
return data[1].map((suggestion) => suggestion[0]);
}
async getComments(video_id, sort_by, comment_id) {
throwIfMissing({ video_id });
const SORT_OPTIONS = {
TOP_COMMENTS: 0,
NEWEST_FIRST: 1
};
const token = GetCommentsSectionParams.encode({
ctx: {
videoId: video_id
},
unkParam: 6,
params: {
opts: {
videoId: video_id,
sortBy: SORT_OPTIONS[sort_by || 'TOP_COMMENTS'],
type: 2,
commentId: comment_id || ''
},
target: 'comments-section'
}
});
const continuation = encodeURIComponent(u8ToBase64(token.finish()));
const continuation_command = new NavigationEndpoint({
continuationCommand: {
request: 'CONTINUATION_REQUEST_TYPE_WATCH_NEXT',
token: continuation
}
});
const response = await continuation_command.call(__classPrivateFieldGet(this, _Innertube_session, "f").actions);
return new Comments(this.actions, response.data);
}
async getHomeFeed() {
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEwhat_to_watch' } });
const response = await browse_endpoint.call(__classPrivateFieldGet(this, _Innertube_session, "f").actions);
return new HomeFeed(this.actions, response);
}
async getGuide() {
const response = await this.actions.execute('/guide');
return new Guide(response.data);
}
async getLibrary() {
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FElibrary' } });
const response = await browse_endpoint.call(__classPrivateFieldGet(this, _Innertube_session, "f").actions);
return new Library(this.actions, response);
}
async getHistory() {
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEhistory' } });
const response = await browse_endpoint.call(__classPrivateFieldGet(this, _Innertube_session, "f").actions);
return new History(this.actions, response);
}
async getTrending() {
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEtrending' } });
const response = await browse_endpoint.call(__classPrivateFieldGet(this, _Innertube_session, "f").actions);
return new TabbedFeed(this.actions, response);
}
async getCourses() {
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEcourses_destination' } });
const response = await browse_endpoint.call(__classPrivateFieldGet(this, _Innertube_session, "f").actions, { parse: true });
return new Feed(this.actions, response);
}
async getSubscriptionsFeed() {
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEsubscriptions' } });
const response = await browse_endpoint.call(__classPrivateFieldGet(this, _Innertube_session, "f").actions, { parse: true });
return new Feed(this.actions, response);
}
async getChannelsFeed() {
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEchannels' } });
const response = await browse_endpoint.call(__classPrivateFieldGet(this, _Innertube_session, "f").actions, { parse: true });
return new Feed(this.actions, response);
}
async getChannel(id) {
throwIfMissing({ id });
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: id } });
const response = await browse_endpoint.call(__classPrivateFieldGet(this, _Innertube_session, "f").actions);
return new Channel(this.actions, response);
}
async getNotifications() {
const response = await this.actions.execute('/notification/get_notification_menu', { notificationsMenuRequestType: 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX' });
return new NotificationsMenu(this.actions, response);
}
async getUnseenNotificationsCount() {
const response = await this.actions.execute('/notification/get_unseen_count');
// FIXME: properly parse this.
return response.data?.unseenCount || response.data?.actions?.[0].updateNotificationsUnseenCountAction?.unseenCount || 0;
}
/**
* Retrieves the user's playlists.
*/
async getPlaylists() {
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEplaylist_aggregation' } });
const response = await browse_endpoint.call(__classPrivateFieldGet(this, _Innertube_session, "f").actions, { parse: true });
return new Feed(this.actions, response);
}
async getPlaylist(id) {
throwIfMissing({ id });
if (!id.startsWith('VL')) {
id = `VL${id}`;
}
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: id } });
const response = await browse_endpoint.call(__classPrivateFieldGet(this, _Innertube_session, "f").actions);
return new Playlist(this.actions, response);
}
async getHashtag(hashtag) {
throwIfMissing({ hashtag });
const writer = Hashtag.encode({
params: {
hashtag,
type: 1
}
});
const params = encodeURIComponent(u8ToBase64(writer.finish()));
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEhashtag', params } });
const response = await browse_endpoint.call(__classPrivateFieldGet(this, _Innertube_session, "f").actions);
return new HashtagFeed(this.actions, response);
}
/**
* An alternative to {@link download}.
* Returns deciphered streaming data.
*
* If you wish to retrieve the video info too, have a look at {@link getBasicInfo} or {@link getInfo}.
* @param video_id - The video id.
* @param options - Format options.
*/
async getStreamingData(video_id, options = {}) {
const info = await this.getBasicInfo(video_id, options?.client);
const format = info.chooseFormat(options);
format.url = format.decipher(__classPrivateFieldGet(this, _Innertube_session, "f").player);
return format;
}
/**
* Downloads a given video. If all you need the direct download link, see {@link getStreamingData}.
* If you wish to retrieve the video info too, have a look at {@link getBasicInfo} or {@link getInfo}.
* @param video_id - The video id.
* @param options - Download options.
*/
async download(video_id, options) {
const info = await this.getBasicInfo(video_id, options?.client);
return info.download(options);
}
/**
* Resolves the given URL.
*/
async resolveURL(url) {
const response = await this.actions.execute('/navigation/resolve_url', { url, parse: true });
if (!response.endpoint)
throw new InnertubeError('Failed to resolve URL. Expected a NavigationEndpoint but got undefined', response);
return response.endpoint;
}
/**
* Gets a post page given a post id and the channel id
*/
async getPost(post_id, channel_id) {
throwIfMissing({ post_id, channel_id });
const writer = CommunityPostParams.encode({
f0: 'community',
f1: {
postId: post_id
},
f2: {
p1: 1,
p2: 1
}
});
const params = encodeURIComponent(u8ToBase64(writer.finish()).replace(/\+/g, '-').replace(/\//g, '_'));
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: channel_id, params: params } });
const response = await browse_endpoint.call(__classPrivateFieldGet(this, _Innertube_session, "f").actions, { parse: true });
return new Feed(this.actions, response);
}
/**
* Gets the comments of a post.
*/
async getPostComments(post_id, channel_id, sort_by) {
throwIfMissing({ post_id, channel_id });
const SORT_OPTIONS = {
TOP_COMMENTS: 0,
NEWEST_FIRST: 1
};
const writer1 = CommunityPostCommentsParam.encode({
title: 'community',
postContainer: {
postId: post_id
},
f0: {
f0: 1,
f1: 1
},
commentDataContainer: {
title: 'comments-section',
commentData: {
sortBy: SORT_OPTIONS[sort_by || 'TOP_COMMENTS'],
f0: 1,
channelId: channel_id,
postId: post_id
}
}
});
const writer2 = CommunityPostCommentsParamContainer.encode({
f0: {
location: 'FEcomment_post_detail_page_web_top_level',
protoData: encodeURIComponent(u8ToBase64(writer1.finish()).replace(/\+/g, '-').replace(/\//g, '_'))
}
});
const continuation = encodeURIComponent(u8ToBase64(writer2.finish()));
const continuation_command = new NavigationEndpoint({
continuationCommand: {
request: 'CONTINUATION_REQUEST_TYPE_BROWSE',
token: continuation
}
});
const response = await continuation_command.call(__classPrivateFieldGet(this, _Innertube_session, "f").actions);
return new Comments(this.actions, response.data);
}
/**
* Fetches an attestation challenge.
*/
async getAttestationChallenge(engagement_type, ids) {
const payload = {
engagementType: engagement_type
};
if (ids)
payload.ids = ids;
return this.actions.execute('/att/get', { parse: true, ...payload });
}
call(endpoint, args) {
return endpoint.call(this.actions, args);
}
/**
* An interface for interacting with YouTube Music.
*/
get music() {
return new Music(__classPrivateFieldGet(this, _Innertube_session, "f"));
}
/**
* An interface for interacting with YouTube Studio.
*/
get studio() {
return new Studio(__classPrivateFieldGet(this, _Innertube_session, "f"));
}
/**
* An interface for interacting with YouTube Kids.
*/
get kids() {
return new Kids(__classPrivateFieldGet(this, _Innertube_session, "f"));
}
/**
* An interface for managing and retrieving account information.
*/
get account() {
return new AccountManager(__classPrivateFieldGet(this, _Innertube_session, "f").actions);
}
/**
* An interface for managing playlists.
*/
get playlist() {
return new PlaylistManager(__classPrivateFieldGet(this, _Innertube_session, "f").actions);
}
/**
* An interface for directly interacting with certain YouTube features.
*/
get interact() {
return new InteractionManager(__classPrivateFieldGet(this, _Innertube_session, "f").actions);
}
/**
* An internal class used to dispatch requests.
*/
get actions() {
return __classPrivateFieldGet(this, _Innertube_session, "f").actions;
}
/**
* The session used by this instance.
*/
get session() {
return __classPrivateFieldGet(this, _Innertube_session, "f");
}
}
_Innertube_session = new WeakMap();
export default Innertube;
//# sourceMappingURL=Innertube.js.map