UI/UX: Adiciona campo opcional para colagem manual de transcrição no VideoMind

This commit is contained in:
2026-06-05 22:19:33 +00:00
parent 281005a8bc
commit 3781c848da
2395 changed files with 242184 additions and 20 deletions
+122
View File
@@ -0,0 +1,122 @@
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 } 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 type Format from './parser/classes/misc/Format.js';
import type { ApiResponse } from './core/Actions.js';
import type { DownloadOptions, EngagementType, FormatOptions, GetVideoInfoOptions, InnerTubeClient, InnerTubeConfig, SearchFilters } from './types/index.js';
import type { IBrowseResponse, IParsedResponse } from './parser/index.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)});
* ```
*/
export default class Innertube {
#private;
constructor(session: Session);
static create(config?: InnerTubeConfig): Promise<Innertube>;
getInfo(target: string | NavigationEndpoint, options?: GetVideoInfoOptions): Promise<VideoInfo>;
getBasicInfo(video_id: string, options?: GetVideoInfoOptions): Promise<VideoInfo>;
getShortsVideoInfo(video_id: string, client?: InnerTubeClient): Promise<ShortFormVideoInfo>;
search(query: string, filters?: SearchFilters): Promise<Search>;
getSearchSuggestions(query: string, previous_query?: string): Promise<string[]>;
getComments(video_id: string, sort_by?: 'TOP_COMMENTS' | 'NEWEST_FIRST', comment_id?: string): Promise<Comments>;
getHomeFeed(): Promise<HomeFeed>;
getGuide(): Promise<Guide>;
getLibrary(): Promise<Library>;
getHistory(): Promise<History>;
getCourses(): Promise<Feed<IBrowseResponse>>;
getSubscriptionsFeed(): Promise<Feed<IBrowseResponse>>;
getChannelsFeed(): Promise<Feed<IBrowseResponse>>;
getChannel(id: string): Promise<Channel>;
getNotifications(): Promise<NotificationsMenu>;
getUnseenNotificationsCount(): Promise<number>;
/**
* Retrieves the user's playlists.
*/
getPlaylists(): Promise<Feed<IBrowseResponse>>;
getPlaylist(id: string): Promise<Playlist>;
getHashtag(hashtag: string): Promise<HashtagFeed>;
/**
* 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.
*/
getStreamingData(video_id: string, options?: FormatOptions): Promise<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.
*/
download(video_id: string, options?: DownloadOptions): Promise<ReadableStream<Uint8Array>>;
/**
* Resolves the given URL.
*/
resolveURL(url: string): Promise<NavigationEndpoint>;
/**
* Gets a post page given a post id and the channel id
*/
getPost(post_id: string, channel_id: string): Promise<Feed<IBrowseResponse>>;
/**
* Gets the comments of a post.
*/
getPostComments(post_id: string, channel_id: string, sort_by?: 'TOP_COMMENTS' | 'NEWEST_FIRST'): Promise<Comments>;
/**
* Fetches an attestation challenge.
*/
getAttestationChallenge(engagement_type: EngagementType, ids?: Record<string, any>[]): Promise<import("./parser/index.js").IGetChallengeResponse>;
/**
* Utility method to call an endpoint without having to use {@link Actions}.
*/
call<T extends IParsedResponse>(endpoint: NavigationEndpoint, args: {
[key: string]: any;
parse: true;
}): Promise<T>;
call(endpoint: NavigationEndpoint, args?: {
[key: string]: any;
parse?: false;
}): Promise<ApiResponse>;
/**
* An interface for interacting with YouTube Music.
*/
get music(): Music;
/**
* An interface for interacting with YouTube Studio.
*/
get studio(): Studio;
/**
* An interface for interacting with YouTube Kids.
*/
get kids(): Kids;
/**
* An interface for managing and retrieving account information.
*/
get account(): AccountManager;
/**
* An interface for managing playlists.
*/
get playlist(): PlaylistManager;
/**
* An interface for directly interacting with certain YouTube features.
*/
get interact(): InteractionManager;
/**
* An internal class used to dispatch requests.
*/
get actions(): import("./core/Actions.js").default;
/**
* The session used by this instance.
*/
get session(): Session;
}
+483
View File
@@ -0,0 +1,483 @@
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 } 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 { NavigateAction } from './parser/continuations.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_Prioritize } 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)});
* ```
*/
export default class Innertube {
#session;
constructor(session) {
this.#session = session;
}
static async create(config = {}) {
return new Innertube(await Session.create(config));
}
async getInfo(target, options) {
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 = this.#session;
const extra_payload = {
playbackContext: {
contentPlaybackContext: {
vis: 0,
splay: false,
lactMilliseconds: '-1',
signatureTimestamp: session.player?.signature_timestamp
}
},
client: options?.client
};
if (options?.po_token) {
extra_payload.serviceIntegrityDimensions = {
poToken: options.po_token
};
}
else 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, options) {
throwIfMissing({ video_id });
const watch_endpoint = new NavigationEndpoint({
watchEndpoint: {
videoId: video_id,
racyCheckOk: true,
contentCheckOk: true
}
});
const session = this.#session;
const extra_payload = {
playbackContext: {
contentPlaybackContext: {
vis: 0,
splay: false,
lactMilliseconds: '-1',
signatureTimestamp: session.player?.signature_timestamp
}
},
client: options?.client
};
if (options?.po_token) {
extra_payload.serviceIntegrityDimensions = {
poToken: options.po_token
};
}
else 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 = this.#session.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.prioritize) {
search_filter.prioritize = SearchFilter_Prioritize[filters.prioritize.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(this.#session.actions);
return new Search(this.actions, response);
}
async getSearchSuggestions(query, previous_query) {
const session = this.#session;
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(this.#session.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(this.#session.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(this.#session.actions);
return new Library(this.actions, response);
}
async getHistory() {
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEhistory' } });
const response = await browse_endpoint.call(this.#session.actions);
return new History(this.actions, response);
}
async getCourses() {
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEcourses_destination' } });
const response = await browse_endpoint.call(this.#session.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(this.#session.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(this.#session.actions, { parse: true });
return new Feed(this.actions, response);
}
async getChannel(id) {
throwIfMissing({ id });
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: id } });
let response = await browse_endpoint.call(this.#session.actions, { parse: true });
if (response.on_response_received_actions?.[0]?.is(NavigateAction)) {
response = await response.on_response_received_actions[0].endpoint.call(this.#session.actions, { parse: true });
}
return new Channel(this.actions, response, true);
}
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(this.#session.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(this.#session.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(this.#session.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);
const format = info.chooseFormat(options);
format.url = await format.decipher(this.#session.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);
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({
f1: {
ucid1: channel_id,
postId: post_id,
ucid2: channel_id
}
});
const params = encodeURIComponent(u8ToBase64(writer.finish()).replace(/\+/g, '-').replace(/\//g, '_'));
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEpost_detail', params: params } });
const response = await browse_endpoint.call(this.#session.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: 'posts',
commentDataContainer: {
title: 'comments-section',
commentData: {
sortBy: SORT_OPTIONS[sort_by || 'TOP_COMMENTS'],
f0: 2,
f1: 0,
channelId: channel_id,
postId: post_id
},
f0: 0
}
});
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(this.#session.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(this.#session);
}
/**
* An interface for interacting with YouTube Studio.
*/
get studio() {
return new Studio(this.#session);
}
/**
* An interface for interacting with YouTube Kids.
*/
get kids() {
return new Kids(this.#session);
}
/**
* An interface for managing and retrieving account information.
*/
get account() {
return new AccountManager(this.#session.actions);
}
/**
* An interface for managing playlists.
*/
get playlist() {
return new PlaylistManager(this.#session.actions);
}
/**
* An interface for directly interacting with certain YouTube features.
*/
get interact() {
return new InteractionManager(this.#session.actions);
}
/**
* An internal class used to dispatch requests.
*/
get actions() {
return this.#session.actions;
}
/**
* The session used by this instance.
*/
get session() {
return this.#session;
}
}
//# sourceMappingURL=Innertube.js.map
File diff suppressed because one or more lines are too long
+45
View File
@@ -0,0 +1,45 @@
import type { IBrowseResponse, IGetChallengeResponse, IGetNotificationsMenuResponse, INextResponse, IParsedResponse, IPlayerResponse, IRawResponse, IResolveURLResponse, ISearchResponse, IUpdatedMetadataResponse } from '../parser/index.js';
import type { Session } from './index.js';
export interface ApiResponse {
success: boolean;
status_code: number;
data: IRawResponse;
}
export type InnertubeEndpoint = '/player' | '/search' | '/browse' | '/next' | '/reel' | '/updated_metadata' | '/notification/get_notification_menu' | '/att/get' | string;
export type ParsedResponse<T> = 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;
export default class Actions {
#private;
session: Session;
constructor(session: Session);
/**
* Makes calls to the playback tracking API.
* @param url - The URL to call.
* @param client - The client to use.
* @param params - Call parameters.
*/
stats(url: string, client: {
client_name: string;
client_version: string;
}, params: {
[key: string]: any;
}): Promise<Response>;
/**
* Executes an API call.
* @param endpoint - The endpoint to call.
* @param args - Call arguments
*/
execute<T extends InnertubeEndpoint>(endpoint: T, args: {
[key: string]: any;
parse: true;
protobuf?: false;
serialized_data?: any;
skip_auth_check?: boolean;
}): Promise<ParsedResponse<T>>;
execute<T extends InnertubeEndpoint>(endpoint: T, args?: {
[key: string]: any;
parse?: false;
protobuf?: true;
serialized_data?: any;
skip_auth_check?: boolean;
}): Promise<ApiResponse>;
}
+111
View File
@@ -0,0 +1,111 @@
import { NavigateAction, Parser } from '../parser/index.js';
import { InnertubeError } from '../utils/Utils.js';
export default class Actions {
session;
constructor(session) {
this.session = session;
}
/**
* Makes calls to the playback tracking API.
* @param url - The URL to call.
* @param client - The client to use.
* @param params - Call parameters.
*/
async stats(url, client, params) {
const s_url = new URL(url);
s_url.searchParams.set('ver', '2');
s_url.searchParams.set('c', client.client_name.toLowerCase());
s_url.searchParams.set('cbrver', client.client_version);
s_url.searchParams.set('cver', client.client_version);
for (const key of Object.keys(params)) {
s_url.searchParams.set(key, params[key]);
}
return await this.session.http.fetch(s_url);
}
async execute(endpoint, args) {
let data;
if (args && !args.protobuf) {
data = { ...args };
if (Reflect.has(data, 'browseId') && !args.skip_auth_check) {
if (this.#needsLogin(data.browseId) && !this.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
}
if (Reflect.has(data, 'skip_auth_check'))
delete data.skip_auth_check;
if (Reflect.has(data, 'override_endpoint'))
delete data.override_endpoint;
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, 'settingItemIdForClient'))
delete data.settingItemIdForClient;
if (Reflect.has(data, 'action')) {
data.actions = [data.action];
delete data.action;
}
if (Reflect.has(data, 'boolValue')) {
data.newValue = { boolValue: data.boolValue };
delete data.boolValue;
}
if (Reflect.has(data, 'token')) {
data.continuation = data.token;
delete data.token;
}
if (data?.client === 'YTMUSIC') {
data.isAudioOnly = true;
}
}
else if (args) {
data = args.serialized_data;
}
const target_endpoint = Reflect.has(args || {}, 'override_endpoint') ? args?.override_endpoint : endpoint;
const response = await this.session.http.fetch(target_endpoint, {
method: 'POST',
body: args?.protobuf ? data : JSON.stringify((data || {})),
headers: {
'Content-Type': args?.protobuf ?
'application/x-protobuf' :
'application/json'
}
});
if (args?.parse) {
let parsed_response = Parser.parseResponse(await response.json());
// Handle redirects
if (this.#isBrowse(parsed_response) && parsed_response.on_response_received_actions?.[0]?.type === 'navigateAction') {
const navigate_action = parsed_response.on_response_received_actions.firstOfType(NavigateAction);
if (navigate_action) {
parsed_response = await navigate_action.endpoint.call(this, { parse: true });
}
}
return parsed_response;
}
// Mimics the Axios API using Fetch's Response object.
return {
success: response.ok,
status_code: response.status,
data: await response.json()
};
}
#isBrowse(response) {
return 'on_response_received_actions' in response;
}
#needsLogin(id) {
return [
'FElibrary',
'FEhistory',
'FEsubscriptions',
'FEchannels',
'FEplaylist_aggregation',
'FEmusic_listening_review',
'FEmusic_library_landing',
'SPaccount_overview',
'SPaccount_notifications',
'SPaccount_privacy',
'SPtime_watched'
].includes(id);
}
}
//# sourceMappingURL=Actions.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"Actions.js","sourceRoot":"","sources":["../../../src/core/Actions.ts"],"names":[],"mappings":"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"}
+49
View File
@@ -0,0 +1,49 @@
import { OAuth2Error } from '../utils/Utils.js';
import type Session from './Session.js';
export type OAuth2ClientID = {
client_id: string;
client_secret: string;
};
export type OAuth2Tokens = {
access_token: string;
expiry_date: string;
expires_in?: number;
refresh_token: string;
scope?: string;
token_type?: string;
client?: OAuth2ClientID;
};
export type DeviceAndUserCode = {
device_code: string;
expires_in: number;
interval: number;
user_code: string;
verification_url: string;
error_code?: string;
};
export type OAuth2AuthEventHandler = (data: {
credentials: OAuth2Tokens;
}) => void;
export type OAuth2AuthPendingEventHandler = (data: DeviceAndUserCode) => void;
export type OAuth2AuthErrorEventHandler = (err: OAuth2Error) => void;
export default class OAuth2 {
#private;
YTTV_URL: URL;
AUTH_SERVER_CODE_URL: URL;
AUTH_SERVER_TOKEN_URL: URL;
AUTH_SERVER_REVOKE_TOKEN_URL: URL;
client_id: OAuth2ClientID | undefined;
oauth2_tokens: OAuth2Tokens | undefined;
constructor(session: Session);
init(tokens?: OAuth2Tokens): Promise<void>;
setTokens(tokens: OAuth2Tokens): void;
cacheCredentials(): Promise<void>;
removeCache(): Promise<void>;
pollForAccessToken(device_and_user_code: DeviceAndUserCode): void;
revokeCredentials(): Promise<Response | undefined>;
refreshAccessToken(): Promise<void>;
getDeviceAndUserCode(): Promise<DeviceAndUserCode>;
getClientID(): Promise<OAuth2ClientID>;
shouldRefreshToken(): boolean;
validateTokens(tokens: OAuth2Tokens): boolean;
}
+223
View File
@@ -0,0 +1,223 @@
import { OAuth2Error, Platform } from '../utils/Utils.js';
import { Log, Constants } from '../utils/index.js';
const TAG = 'OAuth2';
export default class OAuth2 {
#session;
YTTV_URL;
AUTH_SERVER_CODE_URL;
AUTH_SERVER_TOKEN_URL;
AUTH_SERVER_REVOKE_TOKEN_URL;
client_id;
oauth2_tokens;
constructor(session) {
this.#session = session;
this.YTTV_URL = new URL('/tv', Constants.URLS.YT_BASE);
this.AUTH_SERVER_CODE_URL = new URL('/o/oauth2/device/code', Constants.URLS.YT_BASE);
this.AUTH_SERVER_TOKEN_URL = new URL('/o/oauth2/token', Constants.URLS.YT_BASE);
this.AUTH_SERVER_REVOKE_TOKEN_URL = new URL('/o/oauth2/revoke', Constants.URLS.YT_BASE);
}
async init(tokens) {
if (tokens) {
this.setTokens(tokens);
if (this.shouldRefreshToken()) {
await this.refreshAccessToken();
}
this.#session.emit('auth', { credentials: this.oauth2_tokens });
return;
}
const loaded_from_cache = await this.#loadFromCache();
if (loaded_from_cache) {
Log.info(TAG, 'Loaded OAuth2 tokens from cache.', this.oauth2_tokens);
return;
}
if (!this.client_id)
this.client_id = await this.getClientID();
// Initialize OAuth2 flow
const device_and_user_code = await this.getDeviceAndUserCode();
this.#session.emit('auth-pending', device_and_user_code);
this.pollForAccessToken(device_and_user_code);
}
setTokens(tokens) {
const tokensMod = tokens;
// Convert access token remaining lifetime to ISO string
if (tokensMod.expires_in) {
tokensMod.expiry_date = new Date(Date.now() + tokensMod.expires_in * 1000).toISOString();
delete tokensMod.expires_in; // We don't need this anymore
}
if (!this.validateTokens(tokensMod))
throw new OAuth2Error('Invalid tokens provided.');
this.oauth2_tokens = tokensMod;
if (tokensMod.client) {
Log.info(TAG, 'Using provided client id and secret.');
this.client_id = tokensMod.client;
}
}
async cacheCredentials() {
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(this.oauth2_tokens));
await this.#session.cache?.set('youtubei_oauth_credentials', data.buffer);
}
async #loadFromCache() {
const data = await this.#session.cache?.get('youtubei_oauth_credentials');
if (!data)
return false;
const decoder = new TextDecoder();
const credentials = JSON.parse(decoder.decode(data));
this.setTokens(credentials);
this.#session.emit('auth', { credentials });
return true;
}
async removeCache() {
await this.#session.cache?.remove('youtubei_oauth_credentials');
}
pollForAccessToken(device_and_user_code) {
if (!this.client_id)
throw new OAuth2Error('Client ID is missing.');
const { device_code, interval } = device_and_user_code;
const { client_id, client_secret } = this.client_id;
const payload = {
client_id,
client_secret,
code: device_code,
grant_type: 'http://oauth.net/grant_type/device/1.0'
};
const connInterval = setInterval(async () => {
const response = await this.#http.fetch_function(this.AUTH_SERVER_TOKEN_URL, {
body: JSON.stringify(payload),
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const response_data = await response.json();
if (response_data.error) {
switch (response_data.error) {
case 'access_denied':
this.#session.emit('auth-error', new OAuth2Error('Access was denied.', response_data));
clearInterval(connInterval);
break;
case 'expired_token':
this.#session.emit('auth-error', new OAuth2Error('The device code has expired.', response_data));
clearInterval(connInterval);
break;
case 'authorization_pending':
case 'slow_down':
Log.info(TAG, 'Polling for access token...');
break;
default:
this.#session.emit('auth-error', new OAuth2Error('Server returned an unexpected error.', response_data));
clearInterval(connInterval);
break;
}
return;
}
this.setTokens(response_data);
this.#session.emit('auth', { credentials: this.oauth2_tokens });
clearInterval(connInterval);
}, interval * 1000);
}
async revokeCredentials() {
if (!this.oauth2_tokens)
throw new OAuth2Error('Access token not found');
await this.removeCache();
const url = this.AUTH_SERVER_REVOKE_TOKEN_URL;
url.searchParams.set('token', this.oauth2_tokens.access_token);
return this.#session.http.fetch_function(url, { method: 'POST' });
}
async refreshAccessToken() {
if (!this.client_id)
this.client_id = await this.getClientID();
if (!this.oauth2_tokens)
throw new OAuth2Error('No tokens available to refresh.');
const { client_id, client_secret } = this.client_id;
const { refresh_token } = this.oauth2_tokens;
const payload = {
client_id,
client_secret,
refresh_token,
grant_type: 'refresh_token'
};
const response = await this.#http.fetch_function(this.AUTH_SERVER_TOKEN_URL, {
body: JSON.stringify(payload),
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok)
throw new OAuth2Error(`Failed to refresh access token: ${response.status}`);
const response_data = await response.json();
if (response_data.error_code)
throw new OAuth2Error('Authorization server returned an error', response_data);
this.oauth2_tokens.access_token = response_data.access_token;
this.oauth2_tokens.expiry_date = new Date(Date.now() + response_data.expires_in * 1000).toISOString();
this.#session.emit('update-credentials', { credentials: this.oauth2_tokens });
}
async getDeviceAndUserCode() {
if (!this.client_id)
throw new OAuth2Error('Client ID is missing.');
const { client_id } = this.client_id;
const payload = {
client_id,
scope: 'http://gdata.youtube.com https://www.googleapis.com/auth/youtube-paid-content',
device_id: Platform.shim.uuidv4(),
device_model: 'ytlr::'
};
const response = await this.#http.fetch_function(this.AUTH_SERVER_CODE_URL, {
body: JSON.stringify(payload),
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok)
throw new OAuth2Error(`Failed to get device/user code: ${response.status}`);
const response_data = await response.json();
if (response_data.error_code)
throw new OAuth2Error('Authorization server returned an error', response_data);
return response_data;
}
async getClientID() {
const yttv_response = await this.#http.fetch_function(this.YTTV_URL, {
headers: {
'User-Agent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version',
'Referer': 'https://www.youtube.com/tv',
'Accept-Language': 'en-US'
}
});
if (!yttv_response.ok)
throw new OAuth2Error(`Failed to get client ID: ${yttv_response.status}`);
const yttv_response_data = await yttv_response.text();
let script_url_body;
if ((script_url_body = Constants.OAUTH.REGEX.TV_SCRIPT.exec(yttv_response_data)) !== null) {
Log.info(TAG, `Got YouTubeTV script URL (${script_url_body[1]})`);
const script_response = await this.#http.fetch(script_url_body[1], { baseURL: Constants.URLS.YT_BASE });
if (!script_response.ok)
throw new OAuth2Error(`TV script request failed with status code ${script_response.status}`);
const script_response_data = await script_response.text();
const client_identity = script_response_data
.match(Constants.OAUTH.REGEX.CLIENT_IDENTITY);
if (!client_identity || !client_identity.groups)
throw new OAuth2Error('Could not obtain client ID.');
const { client_id, client_secret } = client_identity.groups;
Log.info(TAG, `Client identity retrieved (clientId=${client_id}, clientSecret=${client_secret}).`);
return {
client_id,
client_secret
};
}
throw new OAuth2Error('Could not obtain script URL.');
}
shouldRefreshToken() {
if (!this.oauth2_tokens)
return false;
return Date.now() > new Date(this.oauth2_tokens.expiry_date).getTime();
}
validateTokens(tokens) {
return !(!tokens.access_token || !tokens.refresh_token || !tokens.expiry_date);
}
get #http() {
return this.#session.http;
}
}
//# sourceMappingURL=OAuth2.js.map
File diff suppressed because one or more lines are too long
+25
View File
@@ -0,0 +1,25 @@
import type { FetchFunction, ICache } from '../types/index.js';
import type { BuildScriptResult } from '../utils/javascript/JsExtractor.js';
interface PlayerInitializationOptions {
cache?: ICache;
signature_timestamp: number;
data: BuildScriptResult;
}
/**
* Represents YouTube's player script. This is required to decipher signatures.
*/
export default class Player {
player_id: string;
signature_timestamp: number;
data?: BuildScriptResult | undefined;
po_token?: string;
constructor(player_id: string, signature_timestamp: number, data?: BuildScriptResult | undefined);
static create(cache: ICache | undefined, fetch?: FetchFunction, po_token?: string, player_id?: string): Promise<Player>;
decipher(url?: string, signature_cipher?: string, cipher?: string, this_response_nsig_cache?: Map<string, string>): Promise<string>;
static fromCache(cache: ICache, player_id: string): Promise<Player | null>;
static fromSource(player_id: string, options: PlayerInitializationOptions): Promise<Player>;
cache(cache?: ICache): Promise<void>;
get url(): string;
static get LIBRARY_VERSION(): number;
}
export {};
+213
View File
@@ -0,0 +1,213 @@
import { Constants, BinarySerializer, Log } from '../utils/index.js';
import { getRandomUserAgent, getStringBetweenStrings, getNsigProcessorFn, Platform, PlayerError } from '../utils/Utils.js';
import { JsExtractor, JsAnalyzer } from '../utils/index.js';
import { nsigMatcher, timestampMatcher } from '../utils/javascript/matchers.js';
import packageInfo from '../../package.json' with { type: 'json' };
const TAG = 'Player';
/**
* Represents YouTube's player script. This is required to decipher signatures.
*/
export default class Player {
player_id;
signature_timestamp;
data;
po_token;
constructor(player_id, signature_timestamp, data) {
this.player_id = player_id;
this.signature_timestamp = signature_timestamp;
this.data = data;
}
static async create(cache, fetch = Platform.shim.fetch, po_token, player_id) {
if (!player_id) {
const url = new URL('/iframe_api', Constants.URLS.YT_BASE);
const res = await fetch(url);
if (!res.ok)
throw new PlayerError(`Failed to get player id: ${res.status} (${res.statusText})`);
const js = await res.text();
player_id = getStringBetweenStrings(js, 'player\\/', '\\/');
}
Log.info(TAG, `Using player id (${player_id}). Checking for cached players..`);
if (!player_id)
throw new PlayerError('Failed to get player id');
// We have the player id, now we can check if we have a cached player.
if (cache) {
const cached_player = await Player.fromCache(cache, player_id);
if (cached_player) {
Log.info(TAG, 'Found up-to-date player data in cache.');
cached_player.po_token = po_token;
return cached_player;
}
}
const player_url = new URL(`/s/player/${player_id}/player_es6.vflset/en_US/base.js`, Constants.URLS.YT_BASE);
Log.info(TAG, `Could not find any cached player. Will download a new player from ${player_url}.`);
const player_res = await fetch(player_url, {
headers: {
'user-agent': getRandomUserAgent('desktop')
}
});
if (!player_res.ok) {
throw new PlayerError(`Failed to get player data: ${player_res.status}`);
}
const player_js = await player_res.text();
const nsigFunctionName = 'nsigFunction';
const timestampVarName = 'signatureTimestampVar';
const extractions = [
{ friendlyName: nsigFunctionName, match: nsigMatcher },
{ friendlyName: timestampVarName, match: timestampMatcher, collectDependencies: false }
];
const jsAnalyzer = new JsAnalyzer(player_js, { extractions });
const jsExtractor = new JsExtractor(jsAnalyzer);
const result = jsExtractor.buildScript({
disallowSideEffectInitializers: true,
exportRawValues: true,
rawValueOnly: [timestampVarName]
});
if (result.exportedRawValues && !(timestampVarName in result.exportedRawValues)) {
Log.warn(TAG, 'Failed to extract signature timestamp.');
}
if (!result.exported.includes(nsigFunctionName)) {
Log.warn(TAG, 'Failed to extract n/sig decipher function.');
}
const signatureTimestamp = result.exportedRawValues?.[timestampVarName];
const player = await Player.fromSource(player_id, {
cache,
signature_timestamp: parseInt(signatureTimestamp) || 0,
data: result
});
player.po_token = po_token;
return player;
}
async decipher(url, signature_cipher, cipher, this_response_nsig_cache) {
url = url || signature_cipher || cipher;
if (!url)
throw new PlayerError('No valid URL to decipher');
const args = new URLSearchParams(url);
const url_components = new URL(args.get('url') || url);
const n = url_components.searchParams.get('n');
const s = args.get('s');
const sp = args.get('sp');
if (this.data && ((signature_cipher || cipher) || n)) {
const eval_args = {};
if (signature_cipher || cipher) {
eval_args.sig = s;
eval_args.sp = sp;
}
if (n) {
if (this_response_nsig_cache?.has(n)) {
const nsig = this_response_nsig_cache.get(n);
url_components.searchParams.set('n', nsig);
}
else {
eval_args.n = n;
}
}
if (Object.keys(eval_args).length > 0) {
// Shallow copy to avoid mutating the original data.
const data = { ...this.data };
data.output = `${data.output}\n${getNsigProcessorFn(eval_args.n, eval_args.sp, eval_args.sig)}`;
const result = await Platform.shim.eval(data, eval_args);
if (typeof result !== 'object' || result === null) {
throw new PlayerError('Got invalid result from player script evaluation.');
}
if (typeof eval_args.sig === 'string') {
const signatureResult = result.sig;
Log.info(TAG, `Transformed signature from ${s} to ${signatureResult}.`);
if (typeof signatureResult !== 'string')
throw new PlayerError('Got invalid signature from player script evaluation.');
if (sp) {
url_components.searchParams.set(sp, signatureResult);
}
else {
url_components.searchParams.set('signature', signatureResult);
}
}
if (typeof eval_args.n === 'string') {
const nResult = result.n;
Log.info(TAG, `Transformed n from ${n} to ${nResult}.`);
if (typeof nResult !== 'string')
throw new PlayerError('Failed to decipher nsig');
if (nResult.startsWith('enhanced_except_')) {
Log.warn(TAG, `Decipher script returned an error (n=${n}):`, nResult);
}
else if (this_response_nsig_cache) {
this_response_nsig_cache.set(n, nResult);
}
url_components.searchParams.set('n', nResult);
}
}
}
// @NOTE: SABR requests should include the PoToken (not base64d, but as bytes!) in the payload.
if (url_components.searchParams.get('sabr') !== '1' && this.po_token)
url_components.searchParams.set('pot', this.po_token);
const client = url_components.searchParams.get('c');
switch (client) {
case 'WEB':
url_components.searchParams.set('cver', Constants.CLIENTS.WEB.VERSION);
break;
case 'MWEB':
url_components.searchParams.set('cver', Constants.CLIENTS.MWEB.VERSION);
break;
case 'WEB_REMIX':
url_components.searchParams.set('cver', Constants.CLIENTS.YTMUSIC.VERSION);
break;
case 'WEB_KIDS':
url_components.searchParams.set('cver', Constants.CLIENTS.WEB_KIDS.VERSION);
break;
case 'TVHTML5':
url_components.searchParams.set('cver', Constants.CLIENTS.TV.VERSION);
break;
case 'TVHTML5_SIMPLY':
url_components.searchParams.set('cver', Constants.CLIENTS.TV_SIMPLY.VERSION);
break;
case 'TVHTML5_SIMPLY_EMBEDDED_PLAYER':
url_components.searchParams.set('cver', Constants.CLIENTS.TV_EMBEDDED.VERSION);
break;
case 'WEB_EMBEDDED_PLAYER':
url_components.searchParams.set('cver', Constants.CLIENTS.WEB_EMBEDDED.VERSION);
break;
}
const result = url_components.toString();
Log.info(TAG, `Deciphered URL: ${result}`);
return url_components.toString();
}
static async fromCache(cache, player_id) {
const buffer = await cache.get(player_id);
if (!buffer)
return null;
try {
const deserializedCache = BinarySerializer.deserialize(new Uint8Array(buffer));
if (deserializedCache.libraryVersion !== packageInfo.version) {
Log.warn(TAG, `Cached player data is from a different library version (${deserializedCache.libraryVersion}). Ignoring it.`);
return null;
}
return new Player(deserializedCache.playerId, deserializedCache.signatureTimestamp, deserializedCache.data);
}
catch (e) {
Log.error(TAG, 'Failed to deserialize player data from cache:', e);
return null;
}
}
static async fromSource(player_id, options) {
const player = new Player(player_id, options.signature_timestamp, options.data);
await player.cache(options.cache);
return player;
}
async cache(cache) {
if (!cache || !this.data)
return;
const buffer = BinarySerializer.serialize({
playerId: this.player_id,
signatureTimestamp: this.signature_timestamp,
libraryVersion: packageInfo.version,
data: this.data
});
await cache.set(this.player_id, buffer);
}
get url() {
return new URL(`/s/player/${this.player_id}/player_ias.vflset/en_US/base.js`, Constants.URLS.YT_BASE).toString();
}
static get LIBRARY_VERSION() {
return 14;
}
}
//# sourceMappingURL=Player.js.map
File diff suppressed because one or more lines are too long
+277
View File
@@ -0,0 +1,277 @@
import Actions from './Actions.js';
import OAuth2 from './OAuth2.js';
import Player from './Player.js';
import { EventEmitter, HTTPClient } from '../utils/index.js';
import type { DeviceCategory } from '../utils/Utils.js';
import type { FetchFunction, ICache } from '../types/index.js';
import type { OAuth2Tokens, OAuth2AuthErrorEventHandler, OAuth2AuthPendingEventHandler, OAuth2AuthEventHandler } from './OAuth2.js';
export declare enum ClientType {
WEB = "WEB",
MWEB = "MWEB",
KIDS = "WEB_KIDS",
MUSIC = "WEB_REMIX",
IOS = "iOS",
ANDROID = "ANDROID",
ANDROID_VR = "ANDROID_VR",
ANDROID_MUSIC = "ANDROID_MUSIC",
ANDROID_CREATOR = "ANDROID_CREATOR",
TV = "TVHTML5",
TV_SIMPLY = "TVHTML5_SIMPLY",
TV_EMBEDDED = "TVHTML5_SIMPLY_EMBEDDED_PLAYER",
WEB_EMBEDDED = "WEB_EMBEDDED_PLAYER",
WEB_CREATOR = "WEB_CREATOR"
}
export type Context = {
client: {
hl: string;
gl: string;
remoteHost?: string;
screenDensityFloat?: number;
screenHeightPoints?: number;
screenPixelDensity?: number;
screenWidthPoints?: number;
visitorData?: string;
clientName: string;
clientVersion: string;
clientScreen?: string;
androidSdkVersion?: number;
osName: string;
osVersion: string;
platform: string;
clientFormFactor: string;
userInterfaceTheme?: string;
timeZone: string;
userAgent: string;
browserName?: string;
browserVersion?: string;
originalUrl?: string;
deviceMake: string;
deviceModel: string;
deviceExperimentId?: string;
rolloutToken?: string;
utcOffsetMinutes: number;
mainAppWebInfo?: {
graftUrl: string;
pwaInstallabilityStatus: string;
webDisplayMode: string;
isWebNativeShareAvailable: boolean;
};
memoryTotalKbytes?: string;
configInfo?: {
appInstallData?: string;
coldConfigData?: string;
coldHashData?: string;
hotHashData?: string;
};
kidsAppInfo?: {
categorySettings: {
enabledCategories: string[];
};
contentSettings: {
corpusPreference: string;
kidsNoSearchMode: string;
};
};
};
user: {
enableSafetyMode: boolean;
lockedSafetyMode: boolean;
onBehalfOfUser?: string;
};
thirdParty?: {
embedUrl: string;
};
request?: {
useSsl: boolean;
internalExperimentFlags: any[];
};
};
type ContextData = {
hl: string;
gl: string;
remote_host?: string;
visitor_data: string;
client_name: string;
client_version: string;
user_agent: string;
os_name: string;
os_version: string;
device_category: string;
time_zone: string;
enable_safety_mode: boolean;
browser_name?: string;
browser_version?: string;
app_install_data?: string;
device_make: string;
device_model: string;
on_behalf_of_user?: string;
device_experiment_id?: string;
rollout_token?: string;
};
export type SessionOptions = {
/**
* Language.
*/
lang?: string;
/**
* Geolocation.
*/
location?: string;
/**
* User agent (InnerTube requests only).
*/
user_agent?: string;
/**
* The account index to use. This is useful if you have multiple accounts logged in.
*
* **NOTE:** Only works if you are signed in with cookies.
*/
account_index?: number;
/**
* Specify the Page ID of the YouTube profile/channel to use, if the logged-in account has multiple profiles.
*/
on_behalf_of_user?: string;
/**
* Specifies whether to retrieve the JS player. Disabling this will make session creation faster.
*
* **NOTE:** Deciphering formats is not possible without the JS player.
*/
retrieve_player?: boolean;
/**
* Specifies whether to enable safety mode. This will prevent the session from loading any potentially unsafe content.
*/
enable_safety_mode?: boolean;
/**
* Specifies whether to retrieve the InnerTube config. Useful for "onesie" requests.
*/
retrieve_innertube_config?: boolean;
/**
* Specifies whether to generate the session data locally or retrieve it from YouTube.
* This can be useful if you need more performance.
*
* **NOTE:** If you are using the cache option and a session has already been generated, this will be ignored.
* If you want to force a new session to be generated, you must clear the cache or disable session caching.
*/
generate_session_locally?: boolean;
/**
* If set to `true`, session creation will fail if it's not possible to retrieve session data from YouTube.
* If `false`, a local fallback will be used.
*/
fail_fast?: boolean;
/**
* Specifies whether the session data should be cached.
*/
enable_session_cache?: boolean;
/**
* Platform to use for the session.
*/
device_category?: DeviceCategory;
/**
* InnerTube client type.
*/
client_type?: ClientType;
/**
* The time zone.
*/
timezone?: string;
/**
* Used to cache algorithms, session data, and OAuth2 tokens.
*/
cache?: ICache;
/**
* YouTube cookies.
*/
cookie?: string;
/**
* Setting this to a valid and persistent visitor data string will allow YouTube to give this session tailored content even when not logged in.
* A good way to get a valid one is by either grabbing it from a browser or calling InnerTube's `/visitor_id` endpoint.
*/
visitor_data?: string;
/**
* Fetch function to use.
*/
fetch?: FetchFunction;
/**
* Session bound Proof of Origin Token. This is an attestation token generated by BotGuard/DroidGuard. It is used to confirm that the request is coming from a real client.
*/
po_token?: string;
/**
* Player ID override.
* In most cases, this isn't necessary; but when YouTube introduces breaking changes,
* forcing an older Player ID can help work around temporary issues.
*/
player_id?: string;
};
export type SessionData = {
context: Context;
api_key: string;
api_version: string;
config_data?: string;
};
export type SWSessionData = {
context_data: ContextData;
api_key: string;
api_version: string;
};
export type SessionArgs = {
lang: string;
location: string;
time_zone: string;
user_agent: string;
device_category: DeviceCategory;
client_name: ClientType;
enable_safety_mode: boolean;
visitor_data: string;
on_behalf_of_user: string | undefined;
};
/**
* Represents an InnerTube session. This holds all the data needed to make requests to YouTube.
*/
export default class Session extends EventEmitter {
#private;
context: Context;
api_key: string;
api_version: string;
account_index: number;
config_data?: string | undefined;
player?: Player | undefined;
cookie?: string | undefined;
cache?: ICache | undefined;
po_token?: string | undefined;
oauth: OAuth2;
http: HTTPClient;
logged_in: boolean;
actions: Actions;
user_agent?: string;
constructor(context: Context, api_key: string, api_version: string, account_index: number, config_data?: string | undefined, player?: Player | undefined, cookie?: string | undefined, fetch?: FetchFunction, cache?: ICache | undefined, po_token?: string | undefined);
on(type: 'auth', listener: OAuth2AuthEventHandler): void;
on(type: 'auth-pending', listener: OAuth2AuthPendingEventHandler): void;
on(type: 'auth-error', listener: OAuth2AuthErrorEventHandler): void;
on(type: 'update-credentials', listener: OAuth2AuthEventHandler): void;
once(type: 'auth', listener: OAuth2AuthEventHandler): void;
once(type: 'auth-pending', listener: OAuth2AuthPendingEventHandler): void;
once(type: 'auth-error', listener: OAuth2AuthErrorEventHandler): void;
static create(options?: SessionOptions): Promise<Session>;
/**
* Retrieves session data from cache.
* @param cache - A valid cache implementation.
* @param session_args - User provided session arguments.
*/
static fromCache(cache: ICache, session_args: SessionArgs): Promise<SessionData | null>;
static getSessionData(lang?: string, location?: string, account_index?: number, visitor_data?: string, user_agent?: string, enable_safety_mode?: boolean, generate_session_locally?: boolean, fail_fast?: boolean, device_category?: DeviceCategory, client_name?: ClientType, tz?: string, fetch?: FetchFunction, on_behalf_of_user?: string, cache?: ICache, enable_session_cache?: boolean, po_token?: string, retrieve_innertube_config?: boolean): Promise<{
account_index: number;
context: Context;
api_key: string;
api_version: string;
config_data?: string;
}>;
signIn(credentials?: OAuth2Tokens): Promise<void>;
/**
* Signs out of the current account and revokes the credentials.
*/
signOut(): Promise<Response | undefined>;
get client_version(): string;
get client_name(): string;
get lang(): string;
}
export {};
+371
View File
@@ -0,0 +1,371 @@
import Actions from './Actions.js';
import OAuth2 from './OAuth2.js';
import Player from './Player.js';
import * as Constants from '../utils/Constants.js';
import { EventEmitter, HTTPClient, BinarySerializer, Log, ProtoUtils } from '../utils/index.js';
import { generateRandomString, getRandomUserAgent, InnertubeError, Platform, SessionError } from '../utils/Utils.js';
import packageInfo from '../../package.json' with { type: 'json' };
export const ClientType = {
WEB: "WEB",
MWEB: "MWEB",
KIDS: "WEB_KIDS",
MUSIC: "WEB_REMIX",
IOS: "iOS",
ANDROID: "ANDROID",
ANDROID_VR: "ANDROID_VR",
ANDROID_MUSIC: "ANDROID_MUSIC",
ANDROID_CREATOR: "ANDROID_CREATOR",
TV: "TVHTML5",
TV_SIMPLY: "TVHTML5_SIMPLY",
TV_EMBEDDED: "TVHTML5_SIMPLY_EMBEDDED_PLAYER",
WEB_EMBEDDED: "WEB_EMBEDDED_PLAYER",
WEB_CREATOR: "WEB_CREATOR"
};
const TAG = 'Session';
/**
* Represents an InnerTube session. This holds all the data needed to make requests to YouTube.
*/
export default class Session extends EventEmitter {
context;
api_key;
api_version;
account_index;
config_data;
player;
cookie;
cache;
po_token;
oauth;
http;
logged_in;
actions;
user_agent;
constructor(context, api_key, api_version, account_index, config_data, player, cookie, fetch, cache, po_token) {
super();
this.context = context;
this.api_key = api_key;
this.api_version = api_version;
this.account_index = account_index;
this.config_data = config_data;
this.player = player;
this.cookie = cookie;
this.cache = cache;
this.po_token = po_token;
this.http = new HTTPClient(this, cookie, fetch);
this.actions = new Actions(this);
this.oauth = new OAuth2(this);
this.logged_in = !!cookie;
this.user_agent = context.client.userAgent;
}
on(type, listener) {
super.on(type, listener);
}
once(type, listener) {
super.once(type, listener);
}
static async create(options = {}) {
const { context, api_key, api_version, account_index, config_data } = await Session.getSessionData(options.lang, options.location, options.account_index, options.visitor_data, options.user_agent, options.enable_safety_mode, options.generate_session_locally, options.fail_fast, options.device_category, options.client_type, options.timezone, options.fetch, options.on_behalf_of_user, options.cache, options.enable_session_cache, options.po_token, options.retrieve_innertube_config);
return new Session(context, api_key, api_version, account_index, config_data, options.retrieve_player === false ? undefined : await Player.create(options.cache, options.fetch, options.po_token, options.player_id), options.cookie, options.fetch, options.cache, options.po_token);
}
/**
* Retrieves session data from cache.
* @param cache - A valid cache implementation.
* @param session_args - User provided session arguments.
*/
static async fromCache(cache, session_args) {
const buffer = await cache.get('innertube_session_data');
if (!buffer)
return null;
try {
const session_data = BinarySerializer.deserialize(new Uint8Array(buffer));
if (session_data.library_version !== parseInt(packageInfo.version.split('.', 1)[0])) {
Log.warn(TAG, `Cached session data is from a different library version (${session_data.library_version}). Regenerating session data.`);
return null;
}
if (session_args.visitor_data) {
session_data.context.client.visitorData = session_args.visitor_data;
}
if (session_args.lang)
session_data.context.client.hl = session_args.lang;
if (session_args.location)
session_data.context.client.gl = session_args.location;
if (session_args.on_behalf_of_user)
session_data.context.user.onBehalfOfUser = session_args.on_behalf_of_user;
if (session_args.user_agent)
session_data.context.client.userAgent = session_args.user_agent;
if (session_args.client_name) {
const client = Object.values(Constants.CLIENTS).find((c) => c.NAME === session_args.client_name);
if (client) {
session_data.context.client.clientName = client.NAME;
session_data.context.client.clientVersion = client.VERSION;
}
else
Log.warn(TAG, `Unknown client name: ${session_args.client_name}.`);
}
session_data.context.client.timeZone = session_args.time_zone;
session_data.context.client.platform = session_args.device_category.toUpperCase();
session_data.context.user.enableSafetyMode = session_args.enable_safety_mode;
return session_data;
}
catch (error) {
Log.error(TAG, 'Failed to deserialize session data from cache.', error);
return null;
}
}
static async getSessionData(lang = '', location = '', account_index = 0, visitor_data = '', user_agent = getRandomUserAgent('desktop'), enable_safety_mode = false, generate_session_locally = false, fail_fast = false, device_category = 'desktop', client_name = ClientType.WEB, tz = Intl.DateTimeFormat().resolvedOptions().timeZone, fetch = Platform.shim.fetch, on_behalf_of_user, cache, enable_session_cache = true, po_token, retrieve_innertube_config = true) {
const session_args = {
lang,
location,
time_zone: tz,
user_agent,
device_category,
client_name,
enable_safety_mode,
visitor_data,
on_behalf_of_user,
po_token
};
let session_data;
if (cache && enable_session_cache) {
const cached_session_data = await this.fromCache(cache, session_args);
if (cached_session_data) {
Log.info(TAG, 'Found session data in cache.');
session_data = cached_session_data;
}
}
if (!session_data) {
Log.info(TAG, 'Generating session data.');
let api_key = Constants.CLIENTS.WEB.API_KEY;
let api_version = Constants.CLIENTS.WEB.API_VERSION;
let context_data = {
hl: lang || 'en',
gl: location || 'US',
remote_host: '',
user_agent: user_agent,
visitor_data: visitor_data || ProtoUtils.encodeVisitorData(generateRandomString(11), Math.floor(Date.now() / 1000)),
client_name: client_name,
client_version: Object.values(Constants.CLIENTS).find((v) => v.NAME === client_name)?.VERSION ?? Constants.CLIENTS.WEB.VERSION,
device_category: device_category.toUpperCase(),
os_name: 'Windows',
os_version: '10.0',
time_zone: tz,
browser_name: 'Chrome',
browser_version: '125.0.0.0',
device_make: '',
device_model: '',
enable_safety_mode: enable_safety_mode
};
if (!generate_session_locally) {
try {
const sw_session_data = await this.#getSessionData(session_args, fetch);
api_key = sw_session_data.api_key;
api_version = sw_session_data.api_version;
context_data = sw_session_data.context_data;
}
catch (error) {
if (fail_fast)
throw error;
Log.error(TAG, 'Failed to retrieve session data from server. Session data generated locally will be used instead.', error);
}
}
if (on_behalf_of_user) {
context_data.on_behalf_of_user = on_behalf_of_user;
}
session_data = {
api_key,
api_version,
context: this.#buildContext(context_data)
};
if (retrieve_innertube_config) {
try {
Log.info(TAG, 'Retrieving InnerTube config data.');
const config_headers = {
'Accept-Language': lang,
'Accept': '*/*',
'Referer': Constants.URLS.YT_BASE,
'X-Goog-Visitor-Id': context_data.visitor_data,
'X-Origin': Constants.URLS.YT_BASE,
'X-Youtube-Client-Version': context_data.client_version
};
if (Platform.shim.server) {
config_headers['User-Agent'] = user_agent;
config_headers['Origin'] = Constants.URLS.YT_BASE;
}
const config = await fetch(`${Constants.URLS.API.PRODUCTION_1}v1/config?prettyPrint=false`, {
headers: config_headers,
method: 'POST',
body: JSON.stringify({ context: session_data.context })
});
const configJson = await config.json();
const coldConfigData = configJson.responseContext?.globalConfigGroup?.rawColdConfigGroup?.configData;
const coldHashData = configJson.responseContext?.globalConfigGroup?.coldHashData;
const hotHashData = configJson.responseContext?.globalConfigGroup?.hotHashData;
session_data.config_data = configJson.configData;
session_data.context.client.configInfo = {
...session_data.context.client.configInfo,
coldConfigData,
coldHashData,
hotHashData
};
}
catch (error) {
Log.error(TAG, 'Failed to retrieve config data.', error);
}
}
if (enable_session_cache)
await this.#storeSession(session_data, cache);
}
Log.debug(TAG, 'Session data:', session_data);
return { ...session_data, account_index };
}
static async #storeSession(session_data, cache) {
if (!cache)
return;
Log.info(TAG, 'Compressing and caching session data.');
const buffer = BinarySerializer.serialize({
...session_data,
library_version: parseInt(packageInfo.version)
});
await cache.set('innertube_session_data', buffer);
}
static async #getSessionData(options, fetch = Platform.shim.fetch) {
let visitor_id = generateRandomString(11);
if (options.visitor_data)
visitor_id = this.#getVisitorID(options.visitor_data);
const url = new URL('/sw.js_data', Constants.URLS.YT_BASE);
const res = await fetch(url, {
headers: {
'Accept-Language': options.lang || 'en-US',
'User-Agent': options.user_agent,
'Accept': '*/*',
'Referer': `${Constants.URLS.YT_BASE}/sw.js`,
'Cookie': `PREF=tz=${options.time_zone.replace('/', '.')};VISITOR_INFO1_LIVE=${visitor_id};`
}
});
if (!res.ok)
throw new SessionError(`Failed to retrieve session data: ${res.status}`);
const text = await res.text();
if (!text.startsWith(')]}\''))
throw new SessionError('Invalid JSPB response');
const data = JSON.parse(text.replace(/^\)\]\}'/, ''));
const ytcfg = data[0][2];
const api_version = Constants.CLIENTS.WEB.API_VERSION;
const [[device_info], api_key] = ytcfg;
const config_info = device_info[61];
const app_install_data = config_info[config_info.length - 1];
const context_info = {
hl: options.lang || device_info[0],
gl: options.location || device_info[1],
remote_host: device_info[3],
visitor_data: options.visitor_data || device_info[13],
user_agent: options.user_agent,
client_name: options.client_name,
client_version: options.client_name === 'WEB' ? device_info[16] : Object.values(Constants.CLIENTS).find((c) => c.NAME === options.client_name)?.VERSION || device_info[16],
os_name: device_info[17],
os_version: device_info[18],
time_zone: device_info[79] || options.time_zone,
device_category: options.device_category,
browser_name: device_info[86],
browser_version: device_info[87],
device_make: device_info[11],
device_model: device_info[12],
app_install_data: app_install_data,
device_experiment_id: device_info[103],
rollout_token: device_info[107],
enable_safety_mode: options.enable_safety_mode
};
return { context_data: context_info, api_key, api_version };
}
static #buildContext(args) {
const context = {
client: {
hl: args.hl || 'en',
gl: args.gl || 'US',
remoteHost: args.remote_host,
screenDensityFloat: 1,
screenHeightPoints: 1440,
screenPixelDensity: 1,
screenWidthPoints: 2560,
visitorData: args.visitor_data,
clientName: args.client_name,
clientVersion: args.client_version,
osName: args.os_name,
osVersion: args.os_version,
userAgent: args.user_agent,
platform: args.device_category.toUpperCase(),
clientFormFactor: 'UNKNOWN_FORM_FACTOR',
userInterfaceTheme: 'USER_INTERFACE_THEME_LIGHT',
timeZone: args.time_zone,
originalUrl: Constants.URLS.YT_BASE,
deviceMake: args.device_make,
deviceModel: args.device_model,
browserName: args.browser_name,
browserVersion: args.browser_version,
utcOffsetMinutes: -Math.floor((new Date()).getTimezoneOffset()),
memoryTotalKbytes: '8000000',
rolloutToken: args.rollout_token,
deviceExperimentId: args.device_experiment_id,
mainAppWebInfo: {
graftUrl: Constants.URLS.YT_BASE,
pwaInstallabilityStatus: 'PWA_INSTALLABILITY_STATUS_UNKNOWN',
webDisplayMode: 'WEB_DISPLAY_MODE_BROWSER',
isWebNativeShareAvailable: true
}
},
user: {
enableSafetyMode: args.enable_safety_mode,
lockedSafetyMode: false
},
request: {
useSsl: true,
internalExperimentFlags: []
}
};
if (args.app_install_data)
context.client.configInfo = { appInstallData: args.app_install_data };
if (args.on_behalf_of_user)
context.user.onBehalfOfUser = args.on_behalf_of_user;
return context;
}
static #getVisitorID(visitor_data) {
const decoded_visitor_data = ProtoUtils.decodeVisitorData(visitor_data);
return decoded_visitor_data.id;
}
async signIn(credentials) {
return new Promise(async (resolve, reject) => {
const error_handler = (err) => reject(err);
this.once('auth-error', error_handler);
this.once('auth', () => {
this.off('auth-error', error_handler);
this.logged_in = true;
resolve();
});
try {
await this.oauth.init(credentials);
}
catch (err) {
reject(err);
}
});
}
/**
* Signs out of the current account and revokes the credentials.
*/
async signOut() {
if (!this.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const response = await this.oauth.revokeCredentials();
this.logged_in = false;
return response;
}
get client_version() {
return this.context.client.clientVersion;
}
get client_name() {
return this.context.client.clientName;
}
get lang() {
return this.context.client.hl;
}
}
//# sourceMappingURL=Session.js.map
File diff suppressed because one or more lines are too long
+18
View File
@@ -0,0 +1,18 @@
import { Channel, HomeFeed, Search, VideoInfo } from '../../parser/ytkids/index.js';
import type { Session, ApiResponse } from '../index.js';
import type { GetVideoInfoOptions } from '../../types/index.js';
export default class Kids {
#private;
constructor(session: Session);
search(query: string): Promise<Search>;
getInfo(video_id: string, options?: Omit<GetVideoInfoOptions, 'client'>): Promise<VideoInfo>;
getChannel(channel_id: string): Promise<Channel>;
getHomeFeed(): Promise<HomeFeed>;
/**
* Retrieves the list of supervised accounts that the signed-in user has
* access to, and blocks the given channel for each of them.
* @param channel_id - The channel id to block.
* @returns A list of API responses.
*/
blockChannel(channel_id: string): Promise<ApiResponse[]>;
}
+94
View File
@@ -0,0 +1,94 @@
import { Parser } from '../../parser/index.js';
import { Channel, HomeFeed, Search, VideoInfo } from '../../parser/ytkids/index.js';
import NavigationEndpoint from '../../parser/classes/NavigationEndpoint.js';
import KidsBlocklistPickerItem from '../../parser/classes/ytkids/KidsBlocklistPickerItem.js';
import { InnertubeError, generateRandomString } from '../../utils/Utils.js';
export default class Kids {
#session;
constructor(session) {
this.#session = session;
}
async search(query) {
const search_endpoint = new NavigationEndpoint({ searchEndpoint: { query } });
const response = await search_endpoint.call(this.#session.actions, { client: 'YTKIDS' });
return new Search(this.#session.actions, response);
}
async getInfo(video_id, options) {
const payload = { videoId: video_id };
const watch_endpoint = new NavigationEndpoint({ watchEndpoint: payload });
const watch_next_endpoint = new NavigationEndpoint({ watchNextEndpoint: payload });
const session = this.#session;
const extra_payload = {
playbackContext: {
contentPlaybackContext: {
vis: 0,
splay: false,
lactMilliseconds: '-1',
signatureTimestamp: session.player?.signature_timestamp
}
},
client: 'YTKIDS'
};
if (options?.po_token) {
extra_payload.serviceIntegrityDimensions = {
poToken: options.po_token
};
}
else 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, { client: 'YTKIDS' });
const response = await Promise.all([watch_response, watch_next_response]);
const cpn = generateRandomString(16);
return new VideoInfo(response, session.actions, cpn);
}
async getChannel(channel_id) {
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: channel_id } });
const response = await browse_endpoint.call(this.#session.actions, { client: 'YTKIDS' });
return new Channel(this.#session.actions, response);
}
async getHomeFeed() {
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEkids_home' } });
const response = await browse_endpoint.call(this.#session.actions, { client: 'YTKIDS' });
return new HomeFeed(this.#session.actions, response);
}
/**
* Retrieves the list of supervised accounts that the signed-in user has
* access to, and blocks the given channel for each of them.
* @param channel_id - The channel id to block.
* @returns A list of API responses.
*/
async blockChannel(channel_id) {
const session = this.#session;
if (!session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const kids_blocklist_picker_command = new NavigationEndpoint({
getKidsBlocklistPickerCommand: {
blockedForKidsContent: {
external_channel_id: channel_id
}
}
});
const response = await kids_blocklist_picker_command.call(session.actions, { client: 'YTKIDS' });
const popup = response.data.command.confirmDialogEndpoint;
const popup_fragment = { contents: popup.content, engagementPanels: [] };
const kid_picker = Parser.parseResponse(popup_fragment);
const kids = kid_picker.contents_memo?.getType(KidsBlocklistPickerItem);
if (!kids)
throw new InnertubeError('Could not find any kids profiles or supervised accounts.');
// Iterate through the kids and block the channel if not already blocked.
const responses = [];
for (const kid of kids) {
if (!kid.block_button?.is_toggled) {
kid.setActions(session.actions);
// Block channel and add to the response list.
responses.push(await kid.blockChannel());
}
}
return responses;
}
}
//# sourceMappingURL=Kids.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"Kids.js","sourceRoot":"","sources":["../../../../src/core/clients/Kids.ts"],"names":[],"mappings":"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"}
+34
View File
@@ -0,0 +1,34 @@
import { Album, Artist, Explore, HomeFeed, Library, Playlist, Recap, Search, TrackInfo } from '../../parser/ytmusic/index.js';
import Message from '../../parser/classes/Message.js';
import MusicDescriptionShelf from '../../parser/classes/MusicDescriptionShelf.js';
import MusicResponsiveListItem from '../../parser/classes/MusicResponsiveListItem.js';
import MusicTwoRowItem from '../../parser/classes/MusicTwoRowItem.js';
import NavigationEndpoint from '../../parser/classes/NavigationEndpoint.js';
import PlaylistPanel from '../../parser/classes/PlaylistPanel.js';
import SearchSuggestionsSection from '../../parser/classes/SearchSuggestionsSection.js';
import SectionList from '../../parser/classes/SectionList.js';
import type { ObservedArray } from '../../parser/helpers.js';
import type { GetVideoInfoOptions, MusicSearchFilters } from '../../types/index.js';
import type { Session } from '../index.js';
export default class Music {
#private;
constructor(session: Session);
/**
* Retrieves track info. Passing a list item of type MusicTwoRowItem automatically starts a radio.
* @param target - Video id or a list item.
* @param options - Options for fetching video info.
*/
getInfo(target: string | MusicTwoRowItem | MusicResponsiveListItem | NavigationEndpoint, options?: Omit<GetVideoInfoOptions, 'client'>): Promise<TrackInfo>;
search(query: string, filters?: MusicSearchFilters): Promise<Search>;
getHomeFeed(): Promise<HomeFeed>;
getExplore(): Promise<Explore>;
getLibrary(): Promise<Library>;
getArtist(artist_id: string): Promise<Artist>;
getAlbum(album_id: string): Promise<Album>;
getPlaylist(playlist_id: string): Promise<Playlist>;
getUpNext(video_id: string, automix?: boolean): Promise<PlaylistPanel>;
getRelated(video_id: string): Promise<SectionList | Message>;
getLyrics(video_id: string): Promise<MusicDescriptionShelf | undefined>;
getRecap(): Promise<Recap>;
getSearchSuggestions(input: string): Promise<ObservedArray<SearchSuggestionsSection>>;
}
+230
View File
@@ -0,0 +1,230 @@
import { generateRandomString, InnertubeError, throwIfMissing, u8ToBase64 } from '../../utils/Utils.js';
import { Album, Artist, Explore, HomeFeed, Library, Playlist, Recap, Search, TrackInfo } from '../../parser/ytmusic/index.js';
import AutomixPreviewVideo from '../../parser/classes/AutomixPreviewVideo.js';
import Message from '../../parser/classes/Message.js';
import MusicDescriptionShelf from '../../parser/classes/MusicDescriptionShelf.js';
import MusicQueue from '../../parser/classes/MusicQueue.js';
import MusicResponsiveListItem from '../../parser/classes/MusicResponsiveListItem.js';
import MusicTwoRowItem from '../../parser/classes/MusicTwoRowItem.js';
import NavigationEndpoint from '../../parser/classes/NavigationEndpoint.js';
import PlaylistPanel from '../../parser/classes/PlaylistPanel.js';
import SearchSuggestionsSection from '../../parser/classes/SearchSuggestionsSection.js';
import SectionList from '../../parser/classes/SectionList.js';
import Tab from '../../parser/classes/Tab.js';
import { SearchFilter } from '../../../protos/generated/misc/params.js';
export default class Music {
#session;
#actions;
constructor(session) {
this.#session = session;
this.#actions = session.actions;
}
/**
* Retrieves track info. Passing a list item of type MusicTwoRowItem automatically starts a radio.
* @param target - Video id or a list item.
* @param options - Options for fetching video info.
*/
getInfo(target, options) {
if (target instanceof MusicTwoRowItem) {
return this.#fetchInfoFromEndpoint(target.endpoint, options);
}
else if (target instanceof MusicResponsiveListItem) {
return this.#fetchInfoFromEndpoint(target.overlay?.content?.endpoint ?? target.endpoint, options);
}
else if (target instanceof NavigationEndpoint) {
return this.#fetchInfoFromEndpoint(target, options);
}
return this.#fetchInfoFromVideoId(target, options);
}
async #fetchInfoFromVideoId(video_id, options) {
const payload = { videoId: video_id, racyCheckOk: true, contentCheckOk: true };
const watch_endpoint = new NavigationEndpoint({ watchEndpoint: payload });
const watch_next_endpoint = new NavigationEndpoint({ watchNextEndpoint: payload });
const extra_payload = {
playbackContext: {
contentPlaybackContext: {
vis: 0,
splay: false,
lactMilliseconds: '-1',
signatureTimestamp: this.#session.player?.signature_timestamp
}
},
client: 'YTMUSIC'
};
if (options?.po_token) {
extra_payload.serviceIntegrityDimensions = {
poToken: options.po_token
};
}
else if (this.#session.po_token) {
extra_payload.serviceIntegrityDimensions = {
poToken: this.#session.po_token
};
}
const watch_response = watch_endpoint.call(this.#actions, extra_payload);
const watch_next_response = watch_next_endpoint.call(this.#actions, { client: 'YTMUSIC' });
const response = await Promise.all([watch_response, watch_next_response]);
const cpn = generateRandomString(16);
return new TrackInfo(response, this.#actions, cpn);
}
async #fetchInfoFromEndpoint(endpoint, options) {
if (!endpoint)
throw new Error('This item does not have an endpoint.');
const extra_payload = {
playbackContext: {
contentPlaybackContext: {
vis: 0,
splay: false,
lactMilliseconds: '-1',
signatureTimestamp: this.#session.player?.signature_timestamp
}
},
client: 'YTMUSIC'
};
if (options?.po_token) {
extra_payload.serviceIntegrityDimensions = {
poToken: options.po_token
};
}
else if (this.#session.po_token) {
extra_payload.serviceIntegrityDimensions = {
poToken: this.#session.po_token
};
}
const player_response = endpoint.call(this.#actions, extra_payload);
const next_response = endpoint.call(this.#actions, {
client: 'YTMUSIC',
enablePersistentPlaylistPanel: true,
override_endpoint: '/next'
});
const cpn = generateRandomString(16);
const response = await Promise.all([player_response, next_response]);
return new TrackInfo(response, this.#actions, cpn);
}
async search(query, filters = {}) {
throwIfMissing({ query });
let params;
if (filters.type && filters.type !== 'all') {
const writer = SearchFilter.encode({
filters: {
musicSearchType: {
[filters.type]: true
}
}
});
params = encodeURIComponent(u8ToBase64(writer.finish()));
}
const search_endpoint = new NavigationEndpoint({ searchEndpoint: { query, params } });
const response = await search_endpoint.call(this.#actions, { client: 'YTMUSIC' });
return new Search(response, this.#actions, Reflect.has(filters, 'type') && filters.type !== 'all');
}
async getHomeFeed() {
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEmusic_home' } });
const response = await browse_endpoint.call(this.#actions, { client: 'YTMUSIC' });
return new HomeFeed(response, this.#actions);
}
async getExplore() {
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEmusic_explore' } });
const response = await browse_endpoint.call(this.#actions, { client: 'YTMUSIC' });
return new Explore(response);
// TODO: return new Explore(response, this.#actions);
}
async getLibrary() {
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEmusic_library_landing' } });
const response = await browse_endpoint.call(this.#actions, { client: 'YTMUSIC' });
return new Library(response, this.#actions);
}
async getArtist(artist_id) {
if (!artist_id || !artist_id.startsWith('UC') && !artist_id.startsWith('FEmusic_library_privately_owned_artist'))
throw new InnertubeError('Invalid artist id', artist_id);
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: artist_id } });
const response = await browse_endpoint.call(this.#actions, { client: 'YTMUSIC' });
return new Artist(response, this.#actions);
}
async getAlbum(album_id) {
if (!album_id || !album_id.startsWith('MPR') && !album_id.startsWith('FEmusic_library_privately_owned_release'))
throw new InnertubeError('Invalid album id', album_id);
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: album_id } });
const response = await browse_endpoint.call(this.#actions, { client: 'YTMUSIC' });
return new Album(response);
}
async getPlaylist(playlist_id) {
if (!playlist_id.startsWith('VL'))
playlist_id = `VL${playlist_id}`;
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: playlist_id } });
const response = await browse_endpoint.call(this.#actions, { client: 'YTMUSIC' });
return new Playlist(response, this.#actions);
}
async getUpNext(video_id, automix = true) {
throwIfMissing({ video_id });
const watch_next_endpoint = new NavigationEndpoint({ watchNextEndpoint: { videoId: video_id } });
const response = await watch_next_endpoint.call(this.#actions, { client: 'YTMUSIC', parse: true });
const tabs = response.contents_memo?.getType(Tab);
const tab = tabs?.[0];
if (!tab)
throw new InnertubeError('Could not find target tab.');
const music_queue = tab.content?.as(MusicQueue);
if (!music_queue || !music_queue.content)
throw new InnertubeError('Music queue was empty, the given id is probably invalid.', music_queue);
const playlist_panel = music_queue.content.as(PlaylistPanel);
if (!playlist_panel.playlist_id && automix) {
const automix_preview_video = playlist_panel.contents.firstOfType(AutomixPreviewVideo);
if (!automix_preview_video)
throw new InnertubeError('Automix item not found');
const page = await automix_preview_video.playlist_video?.endpoint.call(this.#actions, {
videoId: video_id,
client: 'YTMUSIC',
parse: true
});
if (!page || !page.contents_memo)
throw new InnertubeError('Could not fetch automix');
return page.contents_memo.getType(PlaylistPanel)[0];
}
return playlist_panel;
}
async getRelated(video_id) {
throwIfMissing({ video_id });
const watch_next_endpoint = new NavigationEndpoint({ watchNextEndpoint: { videoId: video_id } });
const response = await watch_next_endpoint.call(this.#actions, { client: 'YTMUSIC', parse: true });
const tabs = response.contents_memo?.getType(Tab);
const tab = tabs?.find((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === 'MUSIC_PAGE_TYPE_TRACK_RELATED');
if (!tab)
throw new InnertubeError('Could not find target tab.');
const page = await tab.endpoint.call(this.#actions, { client: 'YTMUSIC', parse: true });
if (!page.contents)
throw new InnertubeError('Unexpected response', page);
return page.contents.item().as(SectionList, Message);
}
async getLyrics(video_id) {
throwIfMissing({ video_id });
const watch_next_endpoint = new NavigationEndpoint({ watchNextEndpoint: { videoId: video_id } });
const response = await watch_next_endpoint.call(this.#actions, { client: 'YTMUSIC', parse: true });
const tabs = response.contents_memo?.getType(Tab);
const tab = tabs?.find((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === 'MUSIC_PAGE_TYPE_TRACK_LYRICS');
if (!tab)
throw new InnertubeError('Could not find target tab.');
const page = await tab.endpoint.call(this.#actions, { client: 'YTMUSIC', parse: true });
if (!page.contents)
throw new InnertubeError('Unexpected response', page);
if (page.contents.item().type === 'Message')
throw new InnertubeError(page.contents.item().as(Message).text.toString(), video_id);
const section_list = page.contents.item().as(SectionList).contents;
return section_list.firstOfType(MusicDescriptionShelf);
}
async getRecap() {
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEmusic_listening_review' } });
const response = await browse_endpoint.call(this.#actions, { client: 'YTMUSIC' });
return new Recap(response, this.#actions);
}
async getSearchSuggestions(input) {
const response = await this.#actions.execute('/music/get_search_suggestions', {
input,
client: 'YTMUSIC',
parse: true
});
if (!response.contents_memo)
return [];
return response.contents_memo.getType(SearchSuggestionsSection);
}
}
//# sourceMappingURL=Music.js.map
File diff suppressed because one or more lines are too long
+34
View File
@@ -0,0 +1,34 @@
import type { UpdateVideoMetadataOptions, UploadedVideoMetadataOptions } from '../../types/Misc.js';
import type { ApiResponse, Session } from '../index.js';
export default class Studio {
#private;
constructor(session: Session);
/**
* Updates the metadata of a video.
* @example
* ```ts
* const videoId = 'abcdefg';
* const thumbnail = fs.readFileSync('./my_awesome_thumbnail.jpg');
*
* const response = await yt.studio.updateVideoMetadata(videoId, {
* tags: [ 'astronomy', 'NASA', 'APOD' ],
* title: 'Artemis Mission',
* description: 'A nicely written description...',
* category: 27,
* license: 'creative_commons',
* thumbnail,
* // ...
* });
* ```
*/
updateVideoMetadata(video_id: string, metadata: UpdateVideoMetadataOptions): Promise<ApiResponse>;
/**
* Uploads a video to YouTube.
* @example
* ```ts
* const file = fs.readFileSync('./my_awesome_video.mp4');
* const response = await yt.studio.upload(file.buffer, { title: 'Wow!' });
* ```
*/
upload(file: BodyInit, metadata?: UploadedVideoMetadataOptions): Promise<ApiResponse>;
}
+196
View File
@@ -0,0 +1,196 @@
import { Constants } from '../../utils/index.js';
import { InnertubeError, Platform } from '../../utils/Utils.js';
import { MetadataUpdateRequest } from '../../../protos/generated/youtube/api/pfiinnertube/metadata_update_request.js';
export default class Studio {
#session;
constructor(session) {
this.#session = session;
}
/**
* Updates the metadata of a video.
* @example
* ```ts
* const videoId = 'abcdefg';
* const thumbnail = fs.readFileSync('./my_awesome_thumbnail.jpg');
*
* const response = await yt.studio.updateVideoMetadata(videoId, {
* tags: [ 'astronomy', 'NASA', 'APOD' ],
* title: 'Artemis Mission',
* description: 'A nicely written description...',
* category: 27,
* license: 'creative_commons',
* thumbnail,
* // ...
* });
* ```
*/
async updateVideoMetadata(video_id, metadata) {
const session = this.#session;
if (!session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const payload = {
context: {
client: {
osName: 'Android',
clientName: parseInt(Constants.CLIENT_NAME_IDS.ANDROID),
clientVersion: Constants.CLIENTS.ANDROID.VERSION,
androidSdkVersion: Constants.CLIENTS.ANDROID.SDK_VERSION,
visitorData: session.context.client.visitorData,
osVersion: '13',
acceptLanguage: session.context.client.hl,
acceptRegion: session.context.client.gl,
deviceMake: 'Google',
deviceModel: 'sdk_gphone64_x86_64',
screenHeightPoints: 840,
screenWidthPoints: 432,
configInfo: {
appInstallData: session.context.client.configInfo?.appInstallData
},
timeZone: session.context.client.timeZone,
chipset: 'qcom;taro'
},
activePlayers: []
},
encryptedVideoId: video_id
};
if (metadata.title)
payload.title = { newTitle: metadata.title };
if (metadata.description)
payload.description = { newDescription: metadata.description };
if (metadata.license)
payload.license = { newLicenseId: metadata.license };
if (metadata.tags)
payload.tags = { newTags: metadata.tags };
if (metadata.thumbnail) {
payload.videoStill = {
operation: 3,
image: {
rawBytes: metadata.thumbnail
},
experimentImage: []
};
}
if (Reflect.has(metadata, 'category'))
payload.category = { newCategoryId: metadata.category };
if (Reflect.has(metadata, 'privacy')) {
switch (metadata.privacy) {
case 'PUBLIC':
payload.privacy = { newPrivacy: 1 };
break;
case 'UNLISTED':
payload.privacy = { newPrivacy: 2 };
break;
case 'PRIVATE':
payload.privacy = { newPrivacy: 3 };
break;
default:
throw new Error('Invalid privacy setting');
}
}
if (Reflect.has(metadata, 'made_for_kids')) {
payload.madeForKids = {
operation: 1,
newMfk: metadata.made_for_kids ? 1 : 2
};
}
if (Reflect.has(metadata, 'age_restricted')) {
payload.racy = {
operation: 1,
newRacy: metadata.age_restricted ? 1 : 2
};
}
const writer = MetadataUpdateRequest.encode(payload);
return await session.actions.execute('/video_manager/metadata_update', {
protobuf: true,
serialized_data: writer.finish()
});
}
/**
* Uploads a video to YouTube.
* @example
* ```ts
* const file = fs.readFileSync('./my_awesome_video.mp4');
* const response = await yt.studio.upload(file.buffer, { title: 'Wow!' });
* ```
*/
async upload(file, metadata = {}) {
if (!this.#session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const initial_data = await this.#getInitialUploadData();
const upload_result = await this.#uploadVideo(initial_data.upload_url, file);
if (upload_result.status !== 'STATUS_SUCCESS')
throw new InnertubeError('Could not process video.');
return await this.#setVideoMetadata(initial_data, upload_result, metadata);
}
async #getInitialUploadData() {
const frontend_upload_id = `innertube_android:${Platform.shim.uuidv4()}:0:v=3,api=1,cf=3`;
const payload = {
frontendUploadId: frontend_upload_id,
deviceDisplayName: 'Pixel 6 Pro',
fileId: `goog-edited-video://generated?videoFileUri=content://media/external/video/media/${Platform.shim.uuidv4()}`,
mp4MoovAtomRelocationStatus: 'UNSUPPORTED',
transcodeResult: 'DISABLED',
connectionType: 'WIFI'
};
const response = await this.#session.http.fetch('/upload/youtubei', {
baseURL: Constants.URLS.YT_UPLOAD,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'x-goog-upload-command': 'start',
'x-goog-upload-protocol': 'resumable'
},
body: JSON.stringify(payload)
});
if (!response.ok)
throw new InnertubeError('Could not get initial upload data');
return {
frontend_upload_id,
upload_id: response.headers.get('x-guploader-uploadid'),
upload_url: response.headers.get('x-goog-upload-url'),
scotty_resource_id: response.headers.get('x-goog-upload-header-scotty-resource-id'),
chunk_granularity: response.headers.get('x-goog-upload-chunk-granularity')
};
}
async #uploadVideo(upload_url, file) {
const response = await this.#session.http.fetch_function(upload_url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'x-goog-upload-command': 'upload, finalize',
'x-goog-upload-file-name': `file-${Date.now()}`,
'x-goog-upload-offset': '0'
},
body: file
});
if (!response.ok)
throw new InnertubeError('Could not upload video');
return await response.json();
}
async #setVideoMetadata(initial_data, upload_result, metadata) {
return await this.#session.actions.execute('/upload/createvideo', {
resourceId: {
scottyResourceId: {
id: upload_result.scottyResourceId
}
},
frontendUploadId: initial_data.frontend_upload_id,
initialMetadata: {
title: {
newTitle: metadata.title
},
description: {
newDescription: metadata.description,
shouldSegment: true
},
privacy: {
newPrivacy: metadata.privacy || 'PRIVATE'
},
draftState: {
isDraft: !!metadata.is_draft
}
}
});
}
}
//# sourceMappingURL=Studio.js.map
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
export { default as Kids } from './Kids.js';
export { default as Music } from './Music.js';
export { default as Studio } from './Studio.js';
+4
View File
@@ -0,0 +1,4 @@
export { default as Kids } from './Kids.js';
export { default as Music } from './Music.js';
export { default as Studio } from './Studio.js';
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/core/clients/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,aAAa,CAAC"}
+11
View File
@@ -0,0 +1,11 @@
export { default as Session } from './Session.js';
export * from './Session.js';
export { default as Actions } from './Actions.js';
export * from './Actions.js';
export { default as Player } from './Player.js';
export * from './Player.js';
export { default as OAuth2 } from './OAuth2.js';
export * from './OAuth2.js';
export * as Clients from './clients/index.js';
export * as Managers from './managers/index.js';
export * as Mixins from './mixins/index.js';
+12
View File
@@ -0,0 +1,12 @@
export { default as Session } from './Session.js';
export * from './Session.js';
export { default as Actions } from './Actions.js';
export * from './Actions.js';
export { default as Player } from './Player.js';
export * from './Player.js';
export { default as OAuth2 } from './OAuth2.js';
export * from './OAuth2.js';
export * as Clients from './clients/index.js';
export * as Managers from './managers/index.js';
export * as Mixins from './mixins/index.js';
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,cAAc,CAAC;AAClD,cAAc,cAAc,CAAC;AAE7B,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,cAAc,CAAC;AAClD,cAAc,cAAc,CAAC;AAE7B,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,aAAa,CAAC;AAChD,cAAc,aAAa,CAAC;AAE5B,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,aAAa,CAAC;AAChD,cAAc,aAAa,CAAC;AAE5B,OAAO,KAAK,OAAO,MAAM,oBAAoB,CAAC;AAC9C,OAAO,KAAK,QAAQ,MAAM,qBAAqB,CAAC;AAChD,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC"}
+20
View File
@@ -0,0 +1,20 @@
import type { Actions } from '../index.js';
import AccountInfo from '../../parser/youtube/AccountInfo.js';
import Settings from '../../parser/youtube/Settings.js';
import { AccountItem } from '../../parser/nodes.js';
export default class AccountManager {
#private;
constructor(actions: Actions);
/**
* Retrieves the list of channels belonging to the signed-in account. Only useful when signed in through cookie. If signed in through OAuth, you will get the active channel only.
*/
getInfo(all: true): Promise<AccountItem[]>;
/**
* Retrieves the active channel info for the signed-in account. Throws error if `on_behalf_of_user` was used to create the Innertube instance; use `getInfo(true)` instead.
*/
getInfo(all?: false): Promise<AccountInfo>;
/**
* Gets YouTube settings.
*/
getSettings(): Promise<Settings>;
}
+38
View File
@@ -0,0 +1,38 @@
import AccountInfo from '../../parser/youtube/AccountInfo.js';
import Settings from '../../parser/youtube/Settings.js';
import NavigationEndpoint from '../../parser/classes/NavigationEndpoint.js';
import { InnertubeError } from '../../utils/Utils.js';
import { AccountItem } from '../../parser/nodes.js';
export default class AccountManager {
#actions;
constructor(actions) {
this.#actions = actions;
}
async getInfo(all = false) {
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
if (!all && !!this.#actions.session.context.user.onBehalfOfUser) {
throw new InnertubeError('Boolean argument must be true when "on_behalf_of_user" is specified.');
}
if (all) {
const get_accounts_list_endpoint = new NavigationEndpoint({ getAccountsListInnertubeEndpoint: {
requestType: 'ACCOUNTS_LIST_REQUEST_TYPE_CHANNEL_SWITCHER',
callCircumstance: 'SWITCHING_USERS_FULL'
} });
const response = await get_accounts_list_endpoint.call(this.#actions, { client: 'WEB', parse: true });
return response.actions_memo?.getType(AccountItem) || [];
}
const get_accounts_list_endpoint = new NavigationEndpoint({ getAccountsListInnertubeEndpoint: {} });
const response = await get_accounts_list_endpoint.call(this.#actions, { client: 'TV' });
return new AccountInfo(response);
}
/**
* Gets YouTube settings.
*/
async getSettings() {
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'SPaccount_overview' } });
const response = await browse_endpoint.call(this.#actions);
return new Settings(this.#actions, response);
}
}
//# sourceMappingURL=AccountManager.js.map
@@ -0,0 +1 @@
{"version":3,"file":"AccountManager.js","sourceRoot":"","sources":["../../../../src/core/managers/AccountManager.ts"],"names":[],"mappings":"AAEA,OAAO,WAAW,MAAM,qCAAqC,CAAC;AAC9D,OAAO,QAAQ,MAAM,kCAAkC,CAAC;AACxD,OAAO,kBAAkB,MAAM,4CAA4C,CAAC;AAE5E,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEpD,MAAM,CAAC,OAAO,OAAO,cAAc;IACxB,QAAQ,CAAU;IAE3B,YAAY,OAAgB;QAC1B,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;IAC1B,CAAC;IAUD,KAAK,CAAC,OAAO,CAAC,GAAG,GAAG,KAAK;QACvB,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS;YAClC,MAAM,IAAI,cAAc,CAAC,kDAAkD,CAAC,CAAC;QAE/E,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;YAChE,MAAM,IAAI,cAAc,CAAC,sEAAsE,CAAC,CAAC;QACnG,CAAC;QAED,IAAI,GAAG,EAAE,CAAC;YACR,MAAM,0BAA0B,GAAG,IAAI,kBAAkB,CAAC,EAAE,gCAAgC,EAAE;oBAC5F,WAAW,EAAE,6CAA6C;oBAC1D,gBAAgB,EAAE,sBAAsB;iBACzC,EAAE,CAAC,CAAC;YACL,MAAM,QAAQ,GAAG,MAAM,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACtG,OAAO,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;QAC3D,CAAC;QAED,MAAM,0BAA0B,GAAG,IAAI,kBAAkB,CAAC,EAAE,gCAAgC,EAAE,EAAE,EAAE,CAAC,CAAC;QACpG,MAAM,QAAQ,GAAG,MAAM,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QACxF,OAAO,IAAI,WAAW,CAAC,QAAQ,CAAC,CAAC;IACnC,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,WAAW;QACf,MAAM,eAAe,GAAG,IAAI,kBAAkB,CAAC,EAAE,cAAc,EAAE,EAAE,QAAQ,EAAE,oBAAoB,EAAE,EAAE,CAAC,CAAC;QACvG,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC3D,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC/C,CAAC;CACF"}
@@ -0,0 +1,58 @@
import type { Actions, ApiResponse } from '../index.js';
export default class InteractionManager {
#private;
constructor(actions: Actions);
/**
* Likes a given video.
* @param video_id - The video ID
*/
like(video_id: string): Promise<ApiResponse>;
/**
* Dislikes a given video.
* @param video_id - The video ID
*/
dislike(video_id: string): Promise<ApiResponse>;
/**
* Removes a like/dislike.
* @param video_id - The video ID
*/
removeRating(video_id: string): Promise<ApiResponse>;
/**
* Subscribes to the given channel.
* @param channel_id - The channel ID
*/
subscribe(channel_id: string): Promise<ApiResponse>;
/**
* Unsubscribes from the given channel.
* @param channel_id - The channel ID
*/
unsubscribe(channel_id: string): Promise<ApiResponse>;
/**
* Posts a comment on a given video.
* @param video_id - The video ID
* @param text - The comment text
*/
comment(video_id: string, text: string): Promise<ApiResponse>;
/**
* Translates a given text using YouTube's comment translation feature.
* @param text - The text to translate
* @param target_language - an ISO language code
* @param args - optional arguments
*/
translate(text: string, target_language: string, args?: {
video_id?: string;
comment_id?: string;
}): Promise<{
success: boolean;
status_code: number;
translated_content: any;
data: import("../../parser/index.js").IRawResponse;
}>;
/**
* Changes notification preferences for a given channel.
* Only works with channels you are subscribed to.
* @param channel_id - The channel ID.
* @param type - The notification type.
*/
setNotificationPreferences(channel_id: string, type: 'PERSONALIZED' | 'ALL' | 'NONE'): Promise<ApiResponse>;
}
+163
View File
@@ -0,0 +1,163 @@
import * as ProtoUtils from '../../utils/ProtoUtils.js';
import { throwIfMissing, u8ToBase64 } from '../../utils/Utils.js';
import { CreateCommentParams, NotificationPreferences } from '../../../protos/generated/misc/params.js';
import NavigationEndpoint from '../../parser/classes/NavigationEndpoint.js';
export default class InteractionManager {
#actions;
constructor(actions) {
this.#actions = actions;
}
/**
* Likes a given video.
* @param video_id - The video ID
*/
async like(video_id) {
throwIfMissing({ video_id });
if (!this.#actions.session.logged_in)
throw new Error('You must be signed in to perform this operation.');
const like_endpoint = new NavigationEndpoint({
likeEndpoint: {
status: 'LIKE',
target: video_id
}
});
return like_endpoint.call(this.#actions, { client: 'TV' });
}
/**
* Dislikes a given video.
* @param video_id - The video ID
*/
async dislike(video_id) {
throwIfMissing({ video_id });
if (!this.#actions.session.logged_in)
throw new Error('You must be signed in to perform this operation.');
const dislike_endpoint = new NavigationEndpoint({
likeEndpoint: {
status: 'DISLIKE',
target: video_id
}
});
return dislike_endpoint.call(this.#actions, { client: 'TV' });
}
/**
* Removes a like/dislike.
* @param video_id - The video ID
*/
async removeRating(video_id) {
throwIfMissing({ video_id });
if (!this.#actions.session.logged_in)
throw new Error('You must be signed in to perform this operation.');
const remove_like_endpoint = new NavigationEndpoint({
likeEndpoint: {
status: 'INDIFFERENT',
target: video_id
}
});
return remove_like_endpoint.call(this.#actions, { client: 'TV' });
}
/**
* Subscribes to the given channel.
* @param channel_id - The channel ID
*/
async subscribe(channel_id) {
throwIfMissing({ channel_id });
if (!this.#actions.session.logged_in)
throw new Error('You must be signed in to perform this operation.');
const subscribe_endpoint = new NavigationEndpoint({
subscribeEndpoint: {
channelIds: [channel_id],
params: 'EgIIAhgA'
}
});
return subscribe_endpoint.call(this.#actions);
}
/**
* Unsubscribes from the given channel.
* @param channel_id - The channel ID
*/
async unsubscribe(channel_id) {
throwIfMissing({ channel_id });
if (!this.#actions.session.logged_in)
throw new Error('You must be signed in to perform this operation.');
const unsubscribe_endpoint = new NavigationEndpoint({
unsubscribeEndpoint: {
channelIds: [channel_id],
params: 'CgIIAhgA'
}
});
return unsubscribe_endpoint.call(this.#actions);
}
/**
* Posts a comment on a given video.
* @param video_id - The video ID
* @param text - The comment text
*/
async comment(video_id, text) {
throwIfMissing({ video_id, text });
if (!this.#actions.session.logged_in)
throw new Error('You must be signed in to perform this operation.');
const writer = CreateCommentParams.encode({
videoId: video_id,
params: {
index: 0
},
number: 7
});
const params = encodeURIComponent(u8ToBase64(writer.finish()));
const create_comment_endpoint = new NavigationEndpoint({
createCommentEndpoint: {
commentText: text,
createCommentParams: params
}
});
return create_comment_endpoint.call(this.#actions);
}
/**
* Translates a given text using YouTube's comment translation feature.
* @param text - The text to translate
* @param target_language - an ISO language code
* @param args - optional arguments
*/
async translate(text, target_language, args = {}) {
throwIfMissing({ text, target_language });
const action = ProtoUtils.encodeCommentActionParams(22, { text, target_language, ...args });
const perform_comment_action_endpoint = new NavigationEndpoint({ performCommentActionEndpoint: { action } });
const response = await perform_comment_action_endpoint.call(this.#actions);
const mutation = response.data.frameworkUpdates.entityBatchUpdate.mutations[0].payload.commentEntityPayload;
return {
success: response.success,
status_code: response.status_code,
translated_content: mutation.translatedContent.content,
data: response.data
};
}
/**
* Changes notification preferences for a given channel.
* Only works with channels you are subscribed to.
* @param channel_id - The channel ID.
* @param type - The notification type.
*/
async setNotificationPreferences(channel_id, type) {
throwIfMissing({ channel_id, type });
if (!this.#actions.session.logged_in)
throw new Error('You must be signed in to perform this operation.');
const pref_types = {
PERSONALIZED: 1,
ALL: 2,
NONE: 3
};
if (!Object.keys(pref_types).includes(type.toUpperCase()))
throw new Error(`Invalid notification preference type: ${type}`);
const writer = NotificationPreferences.encode({
channelId: channel_id,
prefId: {
index: pref_types[type.toUpperCase()]
},
number0: 0, number1: 4
});
const params = encodeURIComponent(u8ToBase64(writer.finish()));
const modify_channel_notification_preference_endpoint = new NavigationEndpoint({ modifyChannelNotificationPreferenceEndpoint: { params } });
return modify_channel_notification_preference_endpoint.call(this.#actions);
}
}
//# sourceMappingURL=InteractionManager.js.map
@@ -0,0 +1 @@
{"version":3,"file":"InteractionManager.js","sourceRoot":"","sources":["../../../../src/core/managers/InteractionManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,UAAU,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,0CAA0C,CAAC;AAExG,OAAO,kBAAkB,MAAM,4CAA4C,CAAC;AAE5E,MAAM,CAAC,OAAO,OAAO,kBAAkB;IAC5B,QAAQ,CAAU;IAE3B,YAAY,OAAgB;QAC1B,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;IAC1B,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,IAAI,CAAC,QAAgB;QACzB,cAAc,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;QAE7B,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS;YAClC,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;QAEtE,MAAM,aAAa,GAAG,IAAI,kBAAkB,CAAC;YAC3C,YAAY,EAAE;gBACZ,MAAM,EAAE,MAAM;gBACd,MAAM,EAAE,QAAQ;aACjB;SACF,CAAC,CAAC;QAEH,OAAO,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7D,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,OAAO,CAAC,QAAgB;QAC5B,cAAc,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;QAE7B,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS;YAClC,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;QAEtE,MAAM,gBAAgB,GAAG,IAAI,kBAAkB,CAAC;YAC9C,YAAY,EAAE;gBACZ,MAAM,EAAE,SAAS;gBACjB,MAAM,EAAE,QAAQ;aACjB;SACF,CAAC,CAAC;QAEH,OAAO,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IAChE,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,YAAY,CAAC,QAAgB;QACjC,cAAc,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;QAE7B,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS;YAClC,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;QAEtE,MAAM,oBAAoB,GAAG,IAAI,kBAAkB,CAAC;YAClD,YAAY,EAAE;gBACZ,MAAM,EAAE,aAAa;gBACrB,MAAM,EAAE,QAAQ;aACjB;SACF,CAAC,CAAC;QAEH,OAAO,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IACpE,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,SAAS,CAAC,UAAkB;QAChC,cAAc,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC;QAE/B,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS;YAClC,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;QAEtE,MAAM,kBAAkB,GAAG,IAAI,kBAAkB,CAAC;YAChD,iBAAiB,EAAE;gBACjB,UAAU,EAAE,CAAE,UAAU,CAAE;gBAC1B,MAAM,EAAE,UAAU;aACnB;SACF,CAAC,CAAC;QAEH,OAAO,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChD,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,WAAW,CAAC,UAAkB;QAClC,cAAc,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC;QAE/B,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS;YAClC,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;QAEtE,MAAM,oBAAoB,GAAG,IAAI,kBAAkB,CAAC;YAClD,mBAAmB,EAAE;gBACnB,UAAU,EAAE,CAAE,UAAU,CAAE;gBAC1B,MAAM,EAAE,UAAU;aACnB;SACF,CAAC,CAAC;QAEH,OAAO,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAClD,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,OAAO,CAAC,QAAgB,EAAE,IAAY;QAC1C,cAAc,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QAEnC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS;YAClC,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;QAEtE,MAAM,MAAM,GAAG,mBAAmB,CAAC,MAAM,CAAC;YACxC,OAAO,EAAE,QAAQ;YACjB,MAAM,EAAE;gBACN,KAAK,EAAE,CAAC;aACT;YACD,MAAM,EAAE,CAAC;SACV,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,kBAAkB,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAE/D,MAAM,uBAAuB,GAAG,IAAI,kBAAkB,CAAC;YACrD,qBAAqB,EAAE;gBACrB,WAAW,EAAE,IAAI;gBACjB,mBAAmB,EAAE,MAAM;aAC5B;SACF,CAAC,CAAC;QAEH,OAAO,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACrD,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,SAAS,CAAC,IAAY,EAAE,eAAuB,EAAE,OAAoD,EAAE;QAC3G,cAAc,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,CAAC;QAE1C,MAAM,MAAM,GAAG,UAAU,CAAC,yBAAyB,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;QAE5F,MAAM,+BAA+B,GAAG,IAAI,kBAAkB,CAAC,EAAE,4BAA4B,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;QAC7G,MAAM,QAAQ,GAAG,MAAM,+BAA+B,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC3E,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,gBAAgB,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,oBAAoB,CAAC;QAE5G,OAAO;YACL,OAAO,EAAE,QAAQ,CAAC,OAAO;YACzB,WAAW,EAAE,QAAQ,CAAC,WAAW;YACjC,kBAAkB,EAAE,QAAQ,CAAC,iBAAiB,CAAC,OAAO;YACtD,IAAI,EAAE,QAAQ,CAAC,IAAI;SACpB,CAAC;IACJ,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,0BAA0B,CAAC,UAAkB,EAAE,IAAqC;QACxF,cAAc,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QAErC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS;YAClC,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;QAEtE,MAAM,UAAU,GAAG;YACjB,YAAY,EAAE,CAAC;YACf,GAAG,EAAE,CAAC;YACN,IAAI,EAAE,CAAC;SACR,CAAC;QAEF,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YACvD,MAAM,IAAI,KAAK,CAAC,yCAAyC,IAAI,EAAE,CAAC,CAAC;QAEnE,MAAM,MAAM,GAAG,uBAAuB,CAAC,MAAM,CAAC;YAC5C,SAAS,EAAE,UAAU;YACrB,MAAM,EAAE;gBACN,KAAK,EAAE,UAAU,CAAC,IAAI,CAAC,WAAW,EAA6B,CAAC;aACjE;YACD,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC;SACvB,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,kBAAkB,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAE/D,MAAM,+CAA+C,GAAG,IAAI,kBAAkB,CAAC,EAAE,2CAA2C,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;QAC5I,OAAO,+CAA+C,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC7E,CAAC;CACF"}
+83
View File
@@ -0,0 +1,83 @@
import type { Actions } from '../index.js';
export default class PlaylistManager {
#private;
constructor(actions: Actions);
/**
* Creates a playlist.
* @param title - The title of the playlist.
* @param video_ids - An array of video IDs to add to the playlist.
*/
create(title: string, video_ids: string[]): Promise<{
success: boolean;
status_code: number;
playlist_id?: string;
data: any;
}>;
/**
* Deletes a given playlist.
* @param playlist_id - The playlist ID.
*/
delete(playlist_id: string): Promise<{
playlist_id: string;
success: boolean;
status_code: number;
data: any;
}>;
/**
* Adds a given playlist to the library of a user.
* @param playlist_id - The playlist ID.
*/
addToLibrary(playlist_id: string): Promise<import("../Actions.js").ApiResponse>;
/**
* Remove a given playlist to the library of a user.
* @param playlist_id - The playlist ID.
*/
removeFromLibrary(playlist_id: string): Promise<import("../Actions.js").ApiResponse>;
/**
* Adds videos to a given playlist.
* @param playlist_id - The playlist ID.
* @param video_ids - An array of video IDs to add to the playlist.
*/
addVideos(playlist_id: string, video_ids: string[]): Promise<{
playlist_id: string;
action_result: any;
}>;
/**
* Removes videos from a given playlist.
* @param playlist_id - The playlist ID.
* @param video_ids - An array of video IDs to remove from the playlist.
* @param use_set_video_ids - Option to remove videos using set video IDs.
*/
removeVideos(playlist_id: string, video_ids: string[], use_set_video_ids?: boolean): Promise<{
playlist_id: string;
action_result: any;
}>;
/**
* Moves a video to a new position within a given playlist.
* @param playlist_id - The playlist ID.
* @param moved_video_id - The video ID to move.
* @param predecessor_video_id - The video ID to move the moved video before.
*/
moveVideo(playlist_id: string, moved_video_id: string, predecessor_video_id: string): Promise<{
playlist_id: string;
action_result: any;
}>;
/**
* Sets the name for the given playlist.
* @param playlist_id - The playlist ID.
* @param name - The name / title to use for the playlist.
*/
setName(playlist_id: string, name: string): Promise<{
playlist_id: string;
action_result: any;
}>;
/**
* Sets the description for the given playlist.
* @param playlist_id - The playlist ID.
* @param description - The description to use for the playlist.
*/
setDescription(playlist_id: string, description: string): Promise<{
playlist_id: string;
action_result: any;
}>;
}
+234
View File
@@ -0,0 +1,234 @@
import { InnertubeError, throwIfMissing } from '../../utils/Utils.js';
import Playlist from '../../parser/youtube/Playlist.js';
import NavigationEndpoint from '../../parser/classes/NavigationEndpoint.js';
export default class PlaylistManager {
#actions;
constructor(actions) {
this.#actions = actions;
}
/**
* Creates a playlist.
* @param title - The title of the playlist.
* @param video_ids - An array of video IDs to add to the playlist.
*/
async create(title, video_ids) {
throwIfMissing({ title, video_ids });
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const create_playlist_endpoint = new NavigationEndpoint({
createPlaylistServiceEndpoint: {
title,
videoIds: video_ids
}
});
const response = await create_playlist_endpoint.call(this.#actions);
return {
success: response.success,
status_code: response.status_code,
playlist_id: response.data.playlistId,
data: response.data
};
}
/**
* Deletes a given playlist.
* @param playlist_id - The playlist ID.
*/
async delete(playlist_id) {
throwIfMissing({ playlist_id });
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const delete_playlist_endpoint = new NavigationEndpoint({
deletePlaylistServiceEndpoint: {
sourcePlaylistId: playlist_id
}
});
const response = await delete_playlist_endpoint.call(this.#actions);
return {
playlist_id,
success: response.success,
status_code: response.status_code,
data: response.data
};
}
/**
* Adds a given playlist to the library of a user.
* @param playlist_id - The playlist ID.
*/
async addToLibrary(playlist_id) {
throwIfMissing({ playlist_id });
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const like_playlist_endpoint = new NavigationEndpoint({
likeEndpoint: {
status: 'LIKE',
target: playlist_id
}
});
return await like_playlist_endpoint.call(this.#actions);
}
/**
* Remove a given playlist to the library of a user.
* @param playlist_id - The playlist ID.
*/
async removeFromLibrary(playlist_id) {
throwIfMissing({ playlist_id });
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const remove_like_playlist_endpoint = new NavigationEndpoint({
likeEndpoint: {
status: 'INDIFFERENT',
target: playlist_id
}
});
return await remove_like_playlist_endpoint.call(this.#actions);
}
/**
* Adds videos to a given playlist.
* @param playlist_id - The playlist ID.
* @param video_ids - An array of video IDs to add to the playlist.
*/
async addVideos(playlist_id, video_ids) {
throwIfMissing({ playlist_id, video_ids });
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const playlist_edit_endpoint = new NavigationEndpoint({
playlistEditEndpoint: {
playlistId: playlist_id,
actions: video_ids.map((id) => ({
action: 'ACTION_ADD_VIDEO',
addedVideoId: id
}))
}
});
const response = await playlist_edit_endpoint.call(this.#actions);
return {
playlist_id,
action_result: response.data.actions // TODO: implement actions in the parser
};
}
/**
* Removes videos from a given playlist.
* @param playlist_id - The playlist ID.
* @param video_ids - An array of video IDs to remove from the playlist.
* @param use_set_video_ids - Option to remove videos using set video IDs.
*/
async removeVideos(playlist_id, video_ids, use_set_video_ids = false) {
throwIfMissing({ playlist_id, video_ids });
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const playlist = await this.#getPlaylist(playlist_id);
if (!playlist.info.is_editable)
throw new InnertubeError('This playlist cannot be edited.', playlist_id);
const payload = { playlistId: playlist_id, actions: [] };
const getSetVideoIds = async (pl) => {
const key_id = use_set_video_ids ? 'set_video_id' : 'id';
const videos = pl.videos.filter((video) => video_ids.includes(video.key(key_id).string()));
videos.forEach((video) => payload.actions.push({
action: 'ACTION_REMOVE_VIDEO',
setVideoId: video.key('set_video_id').string()
}));
if (payload.actions.length < video_ids.length) {
const next = await pl.getContinuation();
return getSetVideoIds(next);
}
};
await getSetVideoIds(playlist);
if (!payload.actions.length)
throw new InnertubeError('Given video ids were not found in this playlist.', video_ids);
const playlist_edit_endpoint = new NavigationEndpoint({ playlistEditEndpoint: payload });
const response = await playlist_edit_endpoint.call(this.#actions);
return {
playlist_id,
action_result: response.data.actions // TODO: implement actions in the parser
};
}
/**
* Moves a video to a new position within a given playlist.
* @param playlist_id - The playlist ID.
* @param moved_video_id - The video ID to move.
* @param predecessor_video_id - The video ID to move the moved video before.
*/
async moveVideo(playlist_id, moved_video_id, predecessor_video_id) {
throwIfMissing({ playlist_id, moved_video_id, predecessor_video_id });
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const playlist = await this.#getPlaylist(playlist_id);
if (!playlist.info.is_editable)
throw new InnertubeError('This playlist cannot be edited.', playlist_id);
const payload = { playlistId: playlist_id, actions: [] };
let set_video_id_0, set_video_id_1;
const getSetVideoIds = async (pl) => {
const video_0 = pl.videos.find((video) => moved_video_id === video.key('id').string());
const video_1 = pl.videos.find((video) => predecessor_video_id === video.key('id').string());
set_video_id_0 = set_video_id_0 || video_0?.key('set_video_id').string();
set_video_id_1 = set_video_id_1 || video_1?.key('set_video_id').string();
if (!set_video_id_0 || !set_video_id_1) {
const next = await pl.getContinuation();
return getSetVideoIds(next);
}
};
await getSetVideoIds(playlist);
payload.actions.push({
action: 'ACTION_MOVE_VIDEO_AFTER',
setVideoId: set_video_id_0,
movedSetVideoIdPredecessor: set_video_id_1
});
const playlist_edit_endpoint = new NavigationEndpoint({ playlistEditEndpoint: payload });
const response = await playlist_edit_endpoint.call(this.#actions);
return {
playlist_id,
action_result: response.data.actions // TODO: implement actions in the parser
};
}
async #getPlaylist(playlist_id) {
if (!playlist_id.startsWith('VL')) {
playlist_id = `VL${playlist_id}`;
}
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: playlist_id } });
const browse_response = await browse_endpoint.call(this.#actions, { parse: true });
return new Playlist(this.#actions, browse_response, true);
}
/**
* Sets the name for the given playlist.
* @param playlist_id - The playlist ID.
* @param name - The name / title to use for the playlist.
*/
async setName(playlist_id, name) {
throwIfMissing({ playlist_id, name });
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const payload = { playlist_id, actions: [] };
payload.actions.push({
action: 'ACTION_SET_PLAYLIST_NAME',
playlistName: name
});
const playlist_edit_endpoint = new NavigationEndpoint({ playlistEditEndpoint: payload });
const response = await playlist_edit_endpoint.call(this.#actions);
return {
playlist_id,
action_result: response.data.actions
};
}
/**
* Sets the description for the given playlist.
* @param playlist_id - The playlist ID.
* @param description - The description to use for the playlist.
*/
async setDescription(playlist_id, description) {
throwIfMissing({ playlist_id, description });
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const payload = { playlistId: playlist_id, actions: [] };
payload.actions.push({
action: 'ACTION_SET_PLAYLIST_DESCRIPTION',
playlistDescription: description
});
const playlist_edit_endpoint = new NavigationEndpoint({ playlistEditEndpoint: payload });
const response = await playlist_edit_endpoint.call(this.#actions);
return {
playlist_id,
action_result: response.data.actions
};
}
}
//# sourceMappingURL=PlaylistManager.js.map
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
export { default as AccountManager } from './AccountManager.js';
export { default as PlaylistManager } from './PlaylistManager.js';
export { default as InteractionManager } from './InteractionManager.js';
+4
View File
@@ -0,0 +1,4 @@
export { default as AccountManager } from './AccountManager.js';
export { default as PlaylistManager } from './PlaylistManager.js';
export { default as InteractionManager } from './InteractionManager.js';
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/core/managers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,yBAAyB,CAAC"}
+91
View File
@@ -0,0 +1,91 @@
import type { IParsedResponse } from '../../parser/index.js';
import { ReloadContinuationItemsCommand } from '../../parser/index.js';
import BackstagePost from '../../parser/classes/BackstagePost.js';
import SharedPost from '../../parser/classes/SharedPost.js';
import Channel from '../../parser/classes/Channel.js';
import CompactVideo from '../../parser/classes/CompactVideo.js';
import GridChannel from '../../parser/classes/GridChannel.js';
import GridPlaylist from '../../parser/classes/GridPlaylist.js';
import GridVideo from '../../parser/classes/GridVideo.js';
import LockupView from '../../parser/classes/LockupView.js';
import Playlist from '../../parser/classes/Playlist.js';
import PlaylistPanelVideo from '../../parser/classes/PlaylistPanelVideo.js';
import PlaylistVideo from '../../parser/classes/PlaylistVideo.js';
import Post from '../../parser/classes/Post.js';
import ReelItem from '../../parser/classes/ReelItem.js';
import ShortsLockupView from '../../parser/classes/ShortsLockupView.js';
import ReelShelf from '../../parser/classes/ReelShelf.js';
import RichShelf from '../../parser/classes/RichShelf.js';
import Shelf from '../../parser/classes/Shelf.js';
import Video from '../../parser/classes/Video.js';
import WatchCardCompactVideo from '../../parser/classes/WatchCardCompactVideo.js';
import type { Actions, ApiResponse } from '../index.js';
import type { Memo, ObservedArray } from '../../parser/helpers.js';
import type MusicQueue from '../../parser/classes/MusicQueue.js';
import type RichGrid from '../../parser/classes/RichGrid.js';
import type SectionList from '../../parser/classes/SectionList.js';
import type SecondarySearchContainer from '../../parser/classes/SecondarySearchContainer.js';
import type BrowseFeedActions from '../../parser/classes/BrowseFeedActions.js';
import type ProfileColumn from '../../parser/classes/ProfileColumn.js';
export default class Feed<T extends IParsedResponse = IParsedResponse> {
#private;
constructor(actions: Actions, response: ApiResponse | IParsedResponse, already_parsed?: boolean);
/**
* Get all videos on a given page via memo
*/
static getVideosFromMemo(memo: Memo): ObservedArray<CompactVideo | GridVideo | PlaylistPanelVideo | PlaylistVideo | ReelItem | ShortsLockupView | Video | WatchCardCompactVideo>;
/**
* Get all playlists on a given page via memo
*/
static getPlaylistsFromMemo(memo: Memo): ObservedArray<GridPlaylist | LockupView | Playlist>;
/**
* Get all the videos in the feed
*/
get videos(): ObservedArray<CompactVideo | GridVideo | PlaylistPanelVideo | PlaylistVideo | ReelItem | ShortsLockupView | Video | WatchCardCompactVideo>;
/**
* Get all the community posts in the feed
*/
get posts(): ObservedArray<BackstagePost | Post | SharedPost>;
/**
* Get all the channels in the feed
*/
get channels(): ObservedArray<Channel | GridChannel>;
/**
* Get all playlists in the feed
*/
get playlists(): ObservedArray<GridPlaylist | LockupView | Playlist>;
get memo(): Memo;
/**
* Returns contents from the page.
*/
get page_contents(): SectionList | MusicQueue | RichGrid | ReloadContinuationItemsCommand;
/**
* Returns all segments/sections from the page.
*/
get shelves(): ObservedArray<ReelShelf | RichShelf | Shelf>;
/**
* Finds shelf by title.
*/
getShelf(title: string): ReelShelf | RichShelf | Shelf | undefined;
/**
* Returns secondary contents from the page.
*/
get secondary_contents(): SectionList | SecondarySearchContainer | BrowseFeedActions | ProfileColumn | null;
get actions(): Actions;
/**
* Get the original page data
*/
get page(): T;
/**
* Checks if the feed has continuation.
*/
get has_continuation(): boolean;
/**
* Retrieves continuation data as it is.
*/
getContinuationData(): Promise<T | undefined>;
/**
* Retrieves next batch of contents and returns a new {@link Feed} object.
*/
getContinuation(): Promise<Feed<T>>;
}
+180
View File
@@ -0,0 +1,180 @@
import { Parser, ReloadContinuationItemsCommand } from '../../parser/index.js';
import { concatMemos, InnertubeError } from '../../utils/Utils.js';
import BackstagePost from '../../parser/classes/BackstagePost.js';
import SharedPost from '../../parser/classes/SharedPost.js';
import Channel from '../../parser/classes/Channel.js';
import CompactVideo from '../../parser/classes/CompactVideo.js';
import GridChannel from '../../parser/classes/GridChannel.js';
import GridPlaylist from '../../parser/classes/GridPlaylist.js';
import GridVideo from '../../parser/classes/GridVideo.js';
import LockupView from '../../parser/classes/LockupView.js';
import Playlist from '../../parser/classes/Playlist.js';
import PlaylistPanelVideo from '../../parser/classes/PlaylistPanelVideo.js';
import PlaylistVideo from '../../parser/classes/PlaylistVideo.js';
import Post from '../../parser/classes/Post.js';
import ReelItem from '../../parser/classes/ReelItem.js';
import ShortsLockupView from '../../parser/classes/ShortsLockupView.js';
import ReelShelf from '../../parser/classes/ReelShelf.js';
import RichShelf from '../../parser/classes/RichShelf.js';
import Shelf from '../../parser/classes/Shelf.js';
import Tab from '../../parser/classes/Tab.js';
import Video from '../../parser/classes/Video.js';
import AppendContinuationItemsAction from '../../parser/classes/actions/AppendContinuationItemsAction.js';
import ContinuationItem from '../../parser/classes/ContinuationItem.js';
import TwoColumnBrowseResults from '../../parser/classes/TwoColumnBrowseResults.js';
import TwoColumnSearchResults from '../../parser/classes/TwoColumnSearchResults.js';
import WatchCardCompactVideo from '../../parser/classes/WatchCardCompactVideo.js';
export default class Feed {
#page;
#actions;
#memo;
#continuation;
constructor(actions, response, already_parsed = false) {
if (this.#isParsed(response) || already_parsed) {
this.#page = response;
}
else {
this.#page = Parser.parseResponse(response.data);
}
const memo = concatMemos(...[
this.#page.contents_memo,
this.#page.continuation_contents_memo,
this.#page.on_response_received_commands_memo,
this.#page.on_response_received_endpoints_memo,
this.#page.on_response_received_actions_memo,
this.#page.sidebar_memo,
this.#page.header_memo
]);
if (!memo)
throw new InnertubeError('No memo found in feed');
this.#memo = memo;
this.#actions = actions;
}
#isParsed(response) {
return !('data' in response);
}
/**
* Get all videos on a given page via memo
*/
static getVideosFromMemo(memo) {
return memo.getType(Video, GridVideo, ReelItem, ShortsLockupView, CompactVideo, PlaylistVideo, PlaylistPanelVideo, WatchCardCompactVideo);
}
/**
* Get all playlists on a given page via memo
*/
static getPlaylistsFromMemo(memo) {
const playlists = memo.getType(Playlist, GridPlaylist);
const lockup_views = memo.getType(LockupView)
.filter((lockup) => {
return ['PLAYLIST', 'ALBUM', 'PODCAST'].includes(lockup.content_type);
});
if (lockup_views.length > 0) {
playlists.push(...lockup_views);
}
return playlists;
}
/**
* Get all the videos in the feed
*/
get videos() {
return Feed.getVideosFromMemo(this.#memo);
}
/**
* Get all the community posts in the feed
*/
get posts() {
return this.#memo.getType(BackstagePost, Post, SharedPost);
}
/**
* Get all the channels in the feed
*/
get channels() {
return this.#memo.getType(Channel, GridChannel);
}
/**
* Get all playlists in the feed
*/
get playlists() {
return Feed.getPlaylistsFromMemo(this.#memo);
}
get memo() {
return this.#memo;
}
/**
* Returns contents from the page.
*/
get page_contents() {
const tab_content = this.#memo.getType(Tab)?.[0].content;
const reload_continuation_items = this.#memo.getType(ReloadContinuationItemsCommand)[0];
const append_continuation_items = this.#memo.getType(AppendContinuationItemsAction)[0];
return tab_content || reload_continuation_items || append_continuation_items;
}
/**
* Returns all segments/sections from the page.
*/
get shelves() {
return this.#memo.getType(Shelf, RichShelf, ReelShelf);
}
/**
* Finds shelf by title.
*/
getShelf(title) {
return this.shelves.find((shelf) => shelf.title.toString() === title);
}
/**
* Returns secondary contents from the page.
*/
get secondary_contents() {
if (!this.#page.contents?.is_node)
return null;
const node = this.#page.contents?.item();
if (!node.is(TwoColumnBrowseResults, TwoColumnSearchResults))
return null;
return node.secondary_contents;
}
get actions() {
return this.#actions;
}
/**
* Get the original page data
*/
get page() {
return this.#page;
}
/**
* Checks if the feed has continuation.
*/
get has_continuation() {
return this.#getBodyContinuations().length > 0;
}
/**
* Retrieves continuation data as it is.
*/
async getContinuationData() {
if (this.#continuation) {
if (this.#continuation.length === 0)
throw new InnertubeError('There are no continuations.');
return await this.#continuation[0].endpoint.call(this.#actions, { parse: true });
}
this.#continuation = this.#getBodyContinuations();
if (this.#continuation)
return this.getContinuationData();
}
/**
* Retrieves next batch of contents and returns a new {@link Feed} object.
*/
async getContinuation() {
const continuation_data = await this.getContinuationData();
if (!continuation_data)
throw new InnertubeError('Could not get continuation data');
return new Feed(this.actions, continuation_data, true);
}
#getBodyContinuations() {
if (this.#page.header_memo) {
const header_continuations = this.#page.header_memo.getType(ContinuationItem);
return this.#memo.getType(ContinuationItem).filter((continuation) => !header_continuations.includes(continuation));
}
return this.#memo.getType(ContinuationItem);
}
}
//# sourceMappingURL=Feed.js.map
File diff suppressed because one or more lines are too long
+21
View File
@@ -0,0 +1,21 @@
import Feed from './Feed.js';
import ChipCloudChip from '../../parser/classes/ChipCloudChip.js';
import type { ObservedArray } from '../../parser/helpers.js';
import type { IParsedResponse } from '../../parser/index.js';
import type { ApiResponse, Actions } from '../index.js';
export default class FilterableFeed<T extends IParsedResponse> extends Feed<T> {
#private;
constructor(actions: Actions, data: ApiResponse | T, already_parsed?: boolean);
/**
* Returns the filter chips.
*/
get filter_chips(): ObservedArray<ChipCloudChip>;
/**
* Returns available filters.
*/
get filters(): string[];
/**
* Applies given filter and returns a new {@link Feed} object.
*/
getFilteredFeed(filter: string | ChipCloudChip): Promise<Feed<T>>;
}
+55
View File
@@ -0,0 +1,55 @@
import Feed from './Feed.js';
import ChipCloudChip from '../../parser/classes/ChipCloudChip.js';
import FeedFilterChipBar from '../../parser/classes/FeedFilterChipBar.js';
import { InnertubeError } from '../../utils/Utils.js';
export default class FilterableFeed extends Feed {
#chips;
constructor(actions, data, already_parsed = false) {
super(actions, data, already_parsed);
}
/**
* Returns the filter chips.
*/
get filter_chips() {
if (this.#chips)
return this.#chips || [];
if (this.memo.getType(FeedFilterChipBar)?.length > 1)
throw new InnertubeError('There are too many feed filter chipbars, you\'ll need to find the correct one yourself in this.page');
if (this.memo.getType(FeedFilterChipBar)?.length === 0)
throw new InnertubeError('There are no feed filter chipbars');
this.#chips = this.memo.getType(ChipCloudChip);
return this.#chips || [];
}
/**
* Returns available filters.
*/
get filters() {
return this.filter_chips.map((chip) => chip.text.toString()) || [];
}
/**
* Applies given filter and returns a new {@link Feed} object.
*/
async getFilteredFeed(filter) {
let target_filter;
if (typeof filter === 'string') {
if (!this.filters.includes(filter))
throw new InnertubeError('Filter not found', { available_filters: this.filters });
target_filter = this.filter_chips.find((chip) => chip.text.toString() === filter);
}
else if (filter.type === 'ChipCloudChip') {
target_filter = filter;
}
else {
throw new InnertubeError('Invalid filter');
}
if (!target_filter)
throw new InnertubeError('Filter not found');
if (target_filter.is_selected)
return this;
const response = await target_filter.endpoint?.call(this.actions, { parse: true });
if (!response)
throw new InnertubeError('Failed to get filtered feed');
return new Feed(this.actions, response, true);
}
}
//# sourceMappingURL=FilterableFeed.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"FilterableFeed.js","sourceRoot":"","sources":["../../../../src/core/mixins/FilterableFeed.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,aAAa,MAAM,uCAAuC,CAAC;AAClE,OAAO,iBAAiB,MAAM,2CAA2C,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAMtD,MAAM,CAAC,OAAO,OAAO,cAA0C,SAAQ,IAAO;IAC5E,MAAM,CAAgC;IAEtC,YAAY,OAAgB,EAAE,IAAqB,EAAE,cAAc,GAAG,KAAK;QACzE,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC;IACvC,CAAC;IAED;;OAEG;IACH,IAAI,YAAY;QACd,IAAI,IAAI,CAAC,MAAM;YACb,OAAO,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;QAE3B,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,CAAC;YAClD,MAAM,IAAI,cAAc,CAAC,qGAAqG,CAAC,CAAC;QAElI,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,EAAE,MAAM,KAAK,CAAC;YACpD,MAAM,IAAI,cAAc,CAAC,mCAAmC,CAAC,CAAC;QAEhE,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;QAE/C,OAAO,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;IAC3B,CAAC;IAED;;OAEG;IACH,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;IACrE,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,eAAe,CAAC,MAA8B;QAClD,IAAI,aAAwC,CAAC;QAE7C,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;YAC/B,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAChC,MAAM,IAAI,cAAc,CAAC,kBAAkB,EAAE,EAAE,iBAAiB,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;YACpF,aAAa,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,MAAM,CAAC,CAAC;QACpF,CAAC;aAAM,IAAI,MAAM,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;YAC3C,aAAa,GAAG,MAAM,CAAC;QACzB,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,cAAc,CAAC,gBAAgB,CAAC,CAAC;QAC7C,CAAC;QAED,IAAI,CAAC,aAAa;YAChB,MAAM,IAAI,cAAc,CAAC,kBAAkB,CAAC,CAAC;QAE/C,IAAI,aAAa,CAAC,WAAW;YAC3B,OAAO,IAAI,CAAC;QAEd,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAEnF,IAAI,CAAC,QAAQ;YACX,MAAM,IAAI,cAAc,CAAC,6BAA6B,CAAC,CAAC;QAE1D,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;IAChD,CAAC;CACF"}
+109
View File
@@ -0,0 +1,109 @@
import type { INextResponse, IPlayabilityStatus, IPlayerConfig, IPlayerResponse, IStreamingData } from '../../parser/index.js';
import { TranscriptInfo } from '../../parser/youtube/index.js';
import type { Actions, ApiResponse } from '../index.js';
import type { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../../types/index.js';
import type Format from '../../parser/classes/misc/Format.js';
import type { DashOptions } from '../../types/DashOptions.js';
import type { ObservedArray } from '../../parser/helpers.js';
import type CardCollection from '../../parser/classes/CardCollection.js';
import type Endscreen from '../../parser/classes/Endscreen.js';
import type PlayerAnnotationsExpanded from '../../parser/classes/PlayerAnnotationsExpanded.js';
import type PlayerCaptionsTracklist from '../../parser/classes/PlayerCaptionsTracklist.js';
import type PlayerLiveStoryboardSpec from '../../parser/classes/PlayerLiveStoryboardSpec.js';
import type PlayerStoryboardSpec from '../../parser/classes/PlayerStoryboardSpec.js';
export default class MediaInfo {
#private;
basic_info: {
like_count: number | undefined;
is_liked: boolean | undefined;
is_disliked: boolean | undefined;
embed: {
iframe_url: string;
flash_url: string;
flash_secure_url: string;
width: any;
height: any;
} | null | undefined;
channel: {
id: string;
name: string;
url: string;
} | null;
is_unlisted: boolean | undefined;
is_family_safe: boolean | undefined;
category: string | null;
has_ypc_metadata: boolean | null;
start_timestamp: Date | null;
end_timestamp: Date | null;
view_count: number | undefined;
url_canonical: string | null;
tags: string[] | null;
id?: string | undefined;
channel_id?: string | undefined;
title?: string | undefined;
duration?: number | undefined;
keywords?: string[] | undefined;
is_owner_viewing?: boolean | undefined;
short_description?: string | undefined;
thumbnail?: import("../../parser/misc.js").Thumbnail[] | undefined;
allow_ratings?: boolean | undefined;
author?: string | undefined;
is_private?: boolean | undefined;
is_live?: boolean | undefined;
is_live_content?: boolean | undefined;
is_live_dvr_enabled?: boolean | undefined;
is_upcoming?: boolean | undefined;
is_crawlable?: boolean | undefined;
is_post_live_dvr?: boolean | undefined;
is_low_latency_live_stream?: boolean | undefined;
live_chunk_readahead?: number;
};
annotations?: ObservedArray<PlayerAnnotationsExpanded>;
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec;
endscreen?: Endscreen;
captions?: PlayerCaptionsTracklist;
cards?: CardCollection;
streaming_data?: IStreamingData;
playability_status?: IPlayabilityStatus;
player_config?: IPlayerConfig;
constructor(data: [ApiResponse, ApiResponse?], actions: Actions, cpn: string);
/**
* Generates a DASH manifest from the streaming data.
* @param options
* @returns DASH manifest
*/
toDash(options?: {
url_transformer?: URLTransformer;
format_filter?: FormatFilter;
manifest_options?: DashOptions;
}): Promise<string>;
/**
* Get a cleaned up representation of the adaptive_formats
*/
getStreamingInfo(url_transformer?: URLTransformer, format_filter?: FormatFilter): Promise<import("../../utils/StreamingInfo.js").StreamingInfo>;
/**
* Selects the format that best matches the given options.
* @param options - Options
*/
chooseFormat(options: FormatOptions): Format;
/**
* Downloads the video.
* @param options - Download options.
*/
download(options?: DownloadOptions): Promise<ReadableStream<Uint8Array>>;
/**
* Retrieves the video's transcript.
*/
getTranscript(): Promise<TranscriptInfo>;
addToWatchHistory(client_name?: string, client_version?: string, replacement?: string): Promise<Response>;
updateWatchTime(startTime: number, client_name?: string, client_version?: string, replacement?: string): Promise<Response>;
get actions(): Actions;
/**
* Content Playback Nonce.
*/
get cpn(): string;
/**
* Parsed InnerTube response.
*/
get page(): [IPlayerResponse, INextResponse?];
}
+178
View File
@@ -0,0 +1,178 @@
import { Constants, FormatUtils } from '../../utils/index.js';
import { InnertubeError } from '../../utils/Utils.js';
import { getStreamingInfo } from '../../utils/StreamingInfo.js';
import { Parser } from '../../parser/index.js';
import { TranscriptInfo } from '../../parser/youtube/index.js';
import ContinuationItem from '../../parser/classes/ContinuationItem.js';
import PlayerMicroformat from '../../parser/classes/PlayerMicroformat.js';
import MicroformatData from '../../parser/classes/MicroformatData.js';
export default class MediaInfo {
#page;
#actions;
#cpn;
#playback_tracking;
basic_info;
annotations;
storyboards;
endscreen;
captions;
cards;
streaming_data;
playability_status;
player_config;
constructor(data, actions, cpn) {
this.#actions = actions;
const info = Parser.parseResponse(data[0].data.playerResponse ? data[0].data.playerResponse : data[0].data);
const next = data[1]?.data ? Parser.parseResponse(data[1].data) : undefined;
this.#page = [info, next];
this.#cpn = cpn;
if (info.playability_status?.status === 'ERROR')
throw new InnertubeError('This video is unavailable', info.playability_status);
if (info.microformat && !info.microformat?.is(PlayerMicroformat, MicroformatData))
throw new InnertubeError('Unsupported microformat', info.microformat);
this.basic_info = {
...info.video_details,
/**
* Microformat is a bit redundant, so only
* a few things there are interesting to us.
*/
...{
embed: info.microformat?.is(PlayerMicroformat) ? info.microformat?.embed : null,
channel: info.microformat?.is(PlayerMicroformat) ? info.microformat?.channel : null,
is_unlisted: info.microformat?.is_unlisted,
is_family_safe: info.microformat?.is_family_safe,
category: info.microformat?.is(PlayerMicroformat) ? info.microformat?.category : null,
has_ypc_metadata: info.microformat?.is(PlayerMicroformat) ? info.microformat?.has_ypc_metadata : null,
start_timestamp: info.microformat?.is(PlayerMicroformat) ? info.microformat.start_timestamp : null,
end_timestamp: info.microformat?.is(PlayerMicroformat) ? info.microformat.end_timestamp : null,
view_count: info.microformat?.is(PlayerMicroformat) && isNaN(info.video_details?.view_count) ? info.microformat.view_count : info.video_details?.view_count,
url_canonical: info.microformat?.is(MicroformatData) ? info.microformat?.url_canonical : null,
tags: info.microformat?.is(MicroformatData) ? info.microformat?.tags : null
},
like_count: undefined,
is_liked: undefined,
is_disliked: undefined
};
this.annotations = info.annotations;
this.storyboards = info.storyboards;
this.endscreen = info.endscreen;
this.captions = info.captions;
this.cards = info.cards;
this.streaming_data = info.streaming_data;
this.playability_status = info.playability_status;
this.player_config = info.player_config;
this.#playback_tracking = info.playback_tracking;
}
/**
* Generates a DASH manifest from the streaming data.
* @param options
* @returns DASH manifest
*/
async toDash(options = {}) {
const player_response = this.#page[0];
const manifest_options = options.manifest_options || {};
if (player_response.video_details && (player_response.video_details.is_live)) {
throw new InnertubeError('Generating DASH manifests for live videos is not supported. Please use the DASH and HLS manifests provided by YouTube in `streaming_data.dash_manifest_url` and `streaming_data.hls_manifest_url` instead.');
}
let storyboards;
let captions;
if (manifest_options.include_thumbnails && player_response.storyboards) {
storyboards = player_response.storyboards;
}
if (typeof manifest_options.captions_format === 'string' && player_response.captions?.caption_tracks) {
captions = player_response.captions.caption_tracks;
}
return FormatUtils.toDash(this.streaming_data, this.page[0].video_details?.is_post_live_dvr, options.url_transformer, options.format_filter, this.#cpn, this.#actions.session.player, this.#actions, storyboards, captions, manifest_options);
}
/**
* Get a cleaned up representation of the adaptive_formats
*/
getStreamingInfo(url_transformer, format_filter) {
return getStreamingInfo(this.streaming_data, this.page[0].video_details?.is_post_live_dvr, url_transformer, format_filter, this.cpn, this.#actions.session.player, this.#actions, this.#page[0].storyboards ? this.#page[0].storyboards : undefined);
}
/**
* Selects the format that best matches the given options.
* @param options - Options
*/
chooseFormat(options) {
return FormatUtils.chooseFormat(options, this.streaming_data);
}
/**
* Downloads the video.
* @param options - Download options.
*/
async download(options = {}) {
const player_response = this.#page[0];
if (player_response.video_details && (player_response.video_details.is_live || player_response.video_details.is_post_live_dvr)) {
throw new InnertubeError('Downloading is not supported for live and Post-Live-DVR videos, as they are split up into 5 second segments that are individual files, which require using a tool such as ffmpeg to stitch them together, so they cannot be returned in a single stream.');
}
return FormatUtils.download(options, this.#actions, this.playability_status, this.streaming_data, this.#actions.session.player, this.cpn);
}
/**
* Retrieves the video's transcript.
*/
async getTranscript() {
const next_response = this.page[1];
if (!next_response)
throw new InnertubeError('Cannot get transcript from basic video info.');
if (!next_response.engagement_panels)
throw new InnertubeError('Engagement panels not found. Video likely has no transcript.');
const transcript_panel = next_response.engagement_panels.find((panel) => {
return panel.panel_identifier === 'engagement-panel-searchable-transcript';
});
if (!transcript_panel)
throw new InnertubeError('Transcript panel not found. Video likely has no transcript.');
const transcript_continuation = transcript_panel.content?.as(ContinuationItem);
if (!transcript_continuation)
throw new InnertubeError('Transcript continuation not found.');
const response = await transcript_continuation.endpoint.call(this.actions);
return new TranscriptInfo(this.actions, response);
}
async addToWatchHistory(client_name, client_version, replacement = 'https://www.') {
if (!this.#playback_tracking)
throw new InnertubeError('Playback tracking not available');
const url_params = {
cpn: this.#cpn,
fmt: 251,
rtn: 0,
rt: 0
};
const url = this.#playback_tracking.videostats_playback_url.replace('https://s.', replacement);
return await this.#actions.stats(url, {
client_name: client_name || Constants.CLIENTS.WEB.NAME,
client_version: client_version || Constants.CLIENTS.WEB.VERSION
}, url_params);
}
async updateWatchTime(startTime, client_name = Constants.CLIENTS.WEB.NAME, client_version = Constants.CLIENTS.WEB.VERSION, replacement = 'https://www.') {
if (!this.#playback_tracking)
throw new InnertubeError('Playback tracking not available');
const url_params = {
cpn: this.#cpn,
st: startTime.toFixed(3),
et: startTime.toFixed(3),
cmt: startTime.toFixed(3),
final: '1'
};
const url = this.#playback_tracking.videostats_watchtime_url.replace('https://s.', replacement);
return await this.#actions.stats(url, {
client_name,
client_version
}, url_params);
}
get actions() {
return this.#actions;
}
/**
* Content Playback Nonce.
*/
get cpn() {
return this.#cpn;
}
/**
* Parsed InnerTube response.
*/
get page() {
return this.#page;
}
}
//# sourceMappingURL=MediaInfo.js.map
File diff suppressed because one or more lines are too long
+12
View File
@@ -0,0 +1,12 @@
import { Feed } from './index.js';
import type { Actions, ApiResponse } from '../index.js';
import type { IParsedResponse } from '../../parser/index.js';
export default class TabbedFeed<T extends IParsedResponse> extends Feed<T> {
#private;
constructor(actions: Actions, data: ApiResponse | IParsedResponse, already_parsed?: boolean);
get tabs(): string[];
getTabByName(title: string): Promise<TabbedFeed<T>>;
getTabByURL(url: string): Promise<TabbedFeed<T>>;
hasTabWithURL(url: string): boolean;
get title(): string | undefined;
}
+40
View File
@@ -0,0 +1,40 @@
import { Feed } from './index.js';
import { InnertubeError } from '../../utils/Utils.js';
import Tab from '../../parser/classes/Tab.js';
export default class TabbedFeed extends Feed {
#actions;
#tabs;
constructor(actions, data, already_parsed = false) {
super(actions, data, already_parsed);
this.#actions = actions;
this.#tabs = this.page.contents_memo?.getType(Tab);
}
get tabs() {
return this.#tabs?.map((tab) => tab.title.toString()) ?? [];
}
async getTabByName(title) {
const tab = this.#tabs?.find((tab) => tab.title.toLowerCase() === title.toLowerCase());
if (!tab)
throw new InnertubeError(`Tab "${title}" not found`);
if (tab.selected)
return this;
const response = await tab.endpoint.call(this.#actions);
return new TabbedFeed(this.#actions, response, false);
}
async getTabByURL(url) {
const tab = this.#tabs?.find((tab) => tab.endpoint.metadata.url?.split('/').pop() === url);
if (!tab)
throw new InnertubeError(`Tab "${url}" not found`);
if (tab.selected)
return this;
const response = await tab.endpoint.call(this.#actions);
return new TabbedFeed(this.#actions, response, false);
}
hasTabWithURL(url) {
return this.#tabs?.some((tab) => tab.endpoint.metadata.url?.split('/').pop() === url) ?? false;
}
get title() {
return this.page.contents_memo?.getType(Tab)?.find((tab) => tab.selected)?.title.toString();
}
}
//# sourceMappingURL=TabbedFeed.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"TabbedFeed.js","sourceRoot":"","sources":["../../../../src/core/mixins/TabbedFeed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAClC,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,GAAG,MAAM,6BAA6B,CAAC;AAM9C,MAAM,CAAC,OAAO,OAAO,UAAsC,SAAQ,IAAO;IAC/D,QAAQ,CAAU;IAC3B,KAAK,CAAsB;IAE3B,YAAY,OAAgB,EAAE,IAAmC,EAAE,cAAc,GAAG,KAAK;QACvF,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC;QACrC,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;QACxB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IACrD,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;IAC9D,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,KAAa;QAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;QAEvF,IAAI,CAAC,GAAG;YACN,MAAM,IAAI,cAAc,CAAC,QAAQ,KAAK,aAAa,CAAC,CAAC;QAEvD,IAAI,GAAG,CAAC,QAAQ;YACd,OAAO,IAAI,CAAC;QAEd,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAExD,OAAO,IAAI,UAAU,CAAI,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC3D,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,GAAW;QAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,GAAG,CAAC,CAAC;QAE3F,IAAI,CAAC,GAAG;YACN,MAAM,IAAI,cAAc,CAAC,QAAQ,GAAG,aAAa,CAAC,CAAC;QAErD,IAAI,GAAG,CAAC,QAAQ;YACd,OAAO,IAAI,CAAC;QAEd,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAExD,OAAO,IAAI,UAAU,CAAI,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC3D,CAAC;IAED,aAAa,CAAC,GAAW;QACvB,OAAO,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,GAAG,CAAC,IAAI,KAAK,CAAC;IACjG,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC;IAC9F,CAAC;CACF"}
+4
View File
@@ -0,0 +1,4 @@
export { default as Feed } from './Feed.js';
export { default as FilterableFeed } from './FilterableFeed.js';
export { default as TabbedFeed } from './TabbedFeed.js';
export { default as MediaInfo } from './MediaInfo.js';
+5
View File
@@ -0,0 +1,5 @@
export { default as Feed } from './Feed.js';
export { default as FilterableFeed } from './FilterableFeed.js';
export { default as TabbedFeed } from './TabbedFeed.js';
export { default as MediaInfo } from './MediaInfo.js';
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/core/mixins/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
+10
View File
@@ -0,0 +1,10 @@
import { YTNode } from '../helpers.js';
import { type RawNode } from '../index.js';
import AboutChannelView from './AboutChannelView.js';
import Button from './Button.js';
export default class AboutChannel extends YTNode {
static type: string;
metadata: AboutChannelView | null;
share_channel: Button | null;
constructor(data: RawNode);
}
+15
View File
@@ -0,0 +1,15 @@
import { YTNode } from '../helpers.js';
import { Parser } from '../index.js';
import AboutChannelView from './AboutChannelView.js';
import Button from './Button.js';
export default class AboutChannel extends YTNode {
static type = 'AboutChannel';
metadata;
share_channel;
constructor(data) {
super();
this.metadata = Parser.parseItem(data.metadata, AboutChannelView);
this.share_channel = Parser.parseItem(data.shareChannel, Button);
}
}
//# sourceMappingURL=AboutChannel.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"AboutChannel.js","sourceRoot":"","sources":["../../../../src/parser/classes/AboutChannel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,MAAM,EAAgB,MAAM,aAAa,CAAC;AACnD,OAAO,gBAAgB,MAAM,uBAAuB,CAAC;AACrD,OAAO,MAAM,MAAM,aAAa,CAAC;AAEjC,MAAM,CAAC,OAAO,OAAO,YAAa,SAAQ,MAAM;IAC9C,MAAM,CAAC,IAAI,GAAG,cAAc,CAAC;IAE7B,QAAQ,CAA0B;IAClC,aAAa,CAAgB;IAE7B,YAAY,IAAa;QACvB,KAAK,EAAE,CAAC;QAER,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;QAClE,IAAI,CAAC,aAAa,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IACnE,CAAC"}
+24
View File
@@ -0,0 +1,24 @@
import type { ObservedArray } from '../helpers.js';
import { YTNode } from '../helpers.js';
import { type RawNode } from '../index.js';
import ChannelExternalLinkView from './ChannelExternalLinkView.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import Text from './misc/Text.js';
export default class AboutChannelView extends YTNode {
static type: string;
description?: string;
description_label?: Text;
country?: string;
custom_links_label?: Text;
subscriber_count?: string;
view_count?: string;
joined_date?: Text;
canonical_channel_url?: string;
channel_id?: string;
additional_info_label?: Text;
custom_url_on_tap?: NavigationEndpoint;
video_count?: string;
sign_in_for_business_email?: Text;
links: ObservedArray<ChannelExternalLinkView>;
constructor(data: RawNode);
}
+71
View File
@@ -0,0 +1,71 @@
import { YTNode } from '../helpers.js';
import { Parser } from '../index.js';
import ChannelExternalLinkView from './ChannelExternalLinkView.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import Text from './misc/Text.js';
export default class AboutChannelView extends YTNode {
static type = 'AboutChannelView';
description;
description_label;
country;
custom_links_label;
subscriber_count;
view_count;
joined_date;
canonical_channel_url;
channel_id;
additional_info_label;
custom_url_on_tap;
video_count;
sign_in_for_business_email;
links;
constructor(data) {
super();
if (Reflect.has(data, 'description')) {
this.description = data.description;
}
if (Reflect.has(data, 'descriptionLabel')) {
this.description_label = Text.fromAttributed(data.descriptionLabel);
}
if (Reflect.has(data, 'country')) {
this.country = data.country;
}
if (Reflect.has(data, 'customLinksLabel')) {
this.custom_links_label = Text.fromAttributed(data.customLinksLabel);
}
if (Reflect.has(data, 'subscriberCountText')) {
this.subscriber_count = data.subscriberCountText;
}
if (Reflect.has(data, 'viewCountText')) {
this.view_count = data.viewCountText;
}
if (Reflect.has(data, 'joinedDateText')) {
this.joined_date = Text.fromAttributed(data.joinedDateText);
}
if (Reflect.has(data, 'canonicalChannelUrl')) {
this.canonical_channel_url = data.canonicalChannelUrl;
}
if (Reflect.has(data, 'channelId')) {
this.channel_id = data.channelId;
}
if (Reflect.has(data, 'additionalInfoLabel')) {
this.additional_info_label = Text.fromAttributed(data.additionalInfoLabel);
}
if (Reflect.has(data, 'customUrlOnTap')) {
this.custom_url_on_tap = new NavigationEndpoint(data.customUrlOnTap);
}
if (Reflect.has(data, 'videoCountText')) {
this.video_count = data.videoCountText;
}
if (Reflect.has(data, 'signInForBusinessEmail')) {
this.sign_in_for_business_email = Text.fromAttributed(data.signInForBusinessEmail);
}
if (Reflect.has(data, 'links')) {
this.links = Parser.parseArray(data.links, ChannelExternalLinkView);
}
else {
this.links = [];
}
}
}
//# sourceMappingURL=AboutChannelView.js.map
@@ -0,0 +1 @@
{"version":3,"file":"AboutChannelView.js","sourceRoot":"","sources":["../../../../src/parser/classes/AboutChannelView.ts"],"names":[],"mappings":"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"}
+10
View File
@@ -0,0 +1,10 @@
import Text from './misc/Text.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
export default class AccountChannel extends YTNode {
static type: string;
title: Text;
endpoint: NavigationEndpoint;
constructor(data: RawNode);
}
+14
View File
@@ -0,0 +1,14 @@
import Text from './misc/Text.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import { YTNode } from '../helpers.js';
export default class AccountChannel extends YTNode {
static type = 'AccountChannel';
title;
endpoint;
constructor(data) {
super();
this.title = new Text(data.title);
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
}
}
//# sourceMappingURL=AccountChannel.js.map
@@ -0,0 +1 @@
{"version":3,"file":"AccountChannel.js","sourceRoot":"","sources":["../../../../src/parser/classes/AccountChannel.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,gBAAgB,CAAC;AAClC,OAAO,kBAAkB,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAGvC,MAAM,CAAC,OAAO,OAAO,cAAe,SAAQ,MAAM;IAChD,MAAM,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAE/B,KAAK,CAAO;IACZ,QAAQ,CAAqB;IAE7B,YAAY,IAAa;QACvB,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,KAAK,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAClC,IAAI,CAAC,QAAQ,GAAG,IAAI,kBAAkB,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;IAClE,CAAC"}
+20
View File
@@ -0,0 +1,20 @@
import { YTNode } from '../helpers.js';
import { type RawNode } from '../index.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import Text from './misc/Text.js';
import Thumbnail from './misc/Thumbnail.js';
/**
* Not a real renderer but we treat it as one to keep things organized.
*/
export default class AccountItem extends YTNode {
static type: string;
account_name: Text;
account_photo: Thumbnail[];
is_selected: boolean;
is_disabled: boolean;
has_channel: boolean;
endpoint: NavigationEndpoint;
account_byline: Text;
channel_handle: Text;
constructor(data: RawNode);
}
+30
View File
@@ -0,0 +1,30 @@
import { YTNode } from '../helpers.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import Text from './misc/Text.js';
import Thumbnail from './misc/Thumbnail.js';
/**
* Not a real renderer but we treat it as one to keep things organized.
*/
export default class AccountItem extends YTNode {
static type = 'AccountItem';
account_name;
account_photo;
is_selected;
is_disabled;
has_channel;
endpoint;
account_byline;
channel_handle;
constructor(data) {
super();
this.account_name = new Text(data.accountName);
this.account_photo = Thumbnail.fromResponse(data.accountPhoto);
this.is_selected = !!data.isSelected;
this.is_disabled = !!data.isDisabled;
this.has_channel = !!data.hasChannel;
this.endpoint = new NavigationEndpoint(data.serviceEndpoint);
this.account_byline = new Text(data.accountByline);
this.channel_handle = new Text(data.channelHandle);
}
}
//# sourceMappingURL=AccountItem.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"AccountItem.js","sourceRoot":"","sources":["../../../../src/parser/classes/AccountItem.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC,OAAO,kBAAkB,MAAM,yBAAyB,CAAC;AACzD,OAAO,IAAI,MAAM,gBAAgB,CAAC;AAClC,OAAO,SAAS,MAAM,qBAAqB,CAAC;AAE5C;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,WAAY,SAAQ,MAAM;IAC7C,MAAM,CAAC,IAAI,GAAG,aAAa,CAAC;IAE5B,YAAY,CAAO;IACnB,aAAa,CAAc;IAC3B,WAAW,CAAU;IACrB,WAAW,CAAU;IACrB,WAAW,CAAU;IACrB,QAAQ,CAAqB;IAC7B,cAAc,CAAO;IACrB,cAAc,CAAO;IAErB,YAAY,IAAa;QACvB,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,YAAY,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC/C,IAAI,CAAC,aAAa,GAAG,SAAS,CAAC,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC/D,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;QACrC,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;QACrC,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;QACrC,IAAI,CAAC,QAAQ,GAAG,IAAI,kBAAkB,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QAC7D,IAAI,CAAC,cAAc,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QACnD,IAAI,CAAC,cAAc,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IACrD,CAAC"}
@@ -0,0 +1,11 @@
import AccountItem from './AccountItem.js';
import AccountItemSectionHeader from './AccountItemSectionHeader.js';
import { YTNode, type ObservedArray } from '../helpers.js';
import type { RawNode } from '../index.js';
import CompactLink from './CompactLink.js';
export default class AccountItemSection extends YTNode {
static type: string;
contents: ObservedArray<AccountItem | CompactLink>;
header: AccountItemSectionHeader | null;
constructor(data: RawNode);
}
+16
View File
@@ -0,0 +1,16 @@
import { Parser } from '../index.js';
import AccountItem from './AccountItem.js';
import AccountItemSectionHeader from './AccountItemSectionHeader.js';
import { YTNode } from '../helpers.js';
import CompactLink from './CompactLink.js';
export default class AccountItemSection extends YTNode {
static type = 'AccountItemSection';
contents;
header;
constructor(data) {
super();
this.contents = Parser.parseArray(data.contents, [AccountItem, CompactLink]);
this.header = Parser.parseItem(data.header, AccountItemSectionHeader);
}
}
//# sourceMappingURL=AccountItemSection.js.map
@@ -0,0 +1 @@
{"version":3,"file":"AccountItemSection.js","sourceRoot":"","sources":["../../../../src/parser/classes/AccountItemSection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,WAAW,MAAM,kBAAkB,CAAC;AAC3C,OAAO,wBAAwB,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,MAAM,EAAsB,MAAM,eAAe,CAAC;AAE3D,OAAO,WAAW,MAAM,kBAAkB,CAAC;AAE3C,MAAM,CAAC,OAAO,OAAO,kBAAmB,SAAQ,MAAM;IACpD,MAAM,CAAC,IAAI,GAAG,oBAAoB,CAAC;IAE5B,QAAQ,CAA2C;IACnD,MAAM,CAAkC;IAE/C,YAAY,IAAa;QACvB,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAE,WAAW,EAAE,WAAW,CAAE,CAAC,CAAC;QAC/E,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;IACxE,CAAC"}
@@ -0,0 +1,8 @@
import Text from './misc/Text.js';
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
export default class AccountItemSectionHeader extends YTNode {
static type: string;
title: Text;
constructor(data: RawNode);
}
@@ -0,0 +1,11 @@
import Text from './misc/Text.js';
import { YTNode } from '../helpers.js';
export default class AccountItemSectionHeader extends YTNode {
static type = 'AccountItemSectionHeader';
title;
constructor(data) {
super();
this.title = new Text(data.title);
}
}
//# sourceMappingURL=AccountItemSectionHeader.js.map
@@ -0,0 +1 @@
{"version":3,"file":"AccountItemSectionHeader.js","sourceRoot":"","sources":["../../../../src/parser/classes/AccountItemSectionHeader.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,gBAAgB,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAGvC,MAAM,CAAC,OAAO,OAAO,wBAAyB,SAAQ,MAAM;IAC1D,MAAM,CAAC,IAAI,GAAG,0BAA0B,CAAC;IAEzC,KAAK,CAAO;IAEZ,YAAY,IAAa;QACvB,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,KAAK,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC"}
@@ -0,0 +1,11 @@
import AccountChannel from './AccountChannel.js';
import AccountItemSection from './AccountItemSection.js';
import type { RawNode } from '../index.js';
import type { ObservedArray } from '../helpers.js';
import { YTNode } from '../helpers.js';
export default class AccountSectionList extends YTNode {
static type: string;
contents: ObservedArray<AccountItemSection>;
footers: ObservedArray<AccountChannel>;
constructor(data: RawNode);
}
+15
View File
@@ -0,0 +1,15 @@
import { Parser } from '../index.js';
import AccountChannel from './AccountChannel.js';
import AccountItemSection from './AccountItemSection.js';
import { YTNode } from '../helpers.js';
export default class AccountSectionList extends YTNode {
static type = 'AccountSectionList';
contents;
footers;
constructor(data) {
super();
this.contents = Parser.parseArray(data.contents, AccountItemSection);
this.footers = Parser.parseArray(data.footers, AccountChannel);
}
}
//# sourceMappingURL=AccountSectionList.js.map
@@ -0,0 +1 @@
{"version":3,"file":"AccountSectionList.js","sourceRoot":"","sources":["../../../../src/parser/classes/AccountSectionList.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,cAAc,MAAM,qBAAqB,CAAC;AACjD,OAAO,kBAAkB,MAAM,yBAAyB,CAAC;AAIzD,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC,MAAM,CAAC,OAAO,OAAO,kBAAmB,SAAQ,MAAM;IACpD,MAAM,CAAC,IAAI,GAAG,oBAAoB,CAAC;IAE5B,QAAQ,CAAoC;IAC5C,OAAO,CAAgC;IAE9C,YAAY,IAAa;QACvB,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAC;QACrE,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;IACjE,CAAC"}
@@ -0,0 +1,14 @@
import { YTNode } from '../helpers.js';
import { type RawNode } from '../index.js';
import Text from './misc/Text.js';
import Thumbnail from './misc/Thumbnail.js';
import NavigationEndpoint from './NavigationEndpoint.js';
export default class ActiveAccountHeader extends YTNode {
static type: string;
account_name: Text;
account_photo: Thumbnail[];
endpoint: NavigationEndpoint;
manage_account_title: Text;
channel_handle: Text;
constructor(data: RawNode);
}
@@ -0,0 +1,21 @@
import { YTNode } from '../helpers.js';
import Text from './misc/Text.js';
import Thumbnail from './misc/Thumbnail.js';
import NavigationEndpoint from './NavigationEndpoint.js';
export default class ActiveAccountHeader extends YTNode {
static type = 'ActiveAccountHeader';
account_name;
account_photo;
endpoint;
manage_account_title;
channel_handle;
constructor(data) {
super();
this.account_name = new Text(data.accountName);
this.account_photo = Thumbnail.fromResponse(data.accountPhoto);
this.endpoint = new NavigationEndpoint(data.serviceEndpoint);
this.manage_account_title = new Text(data.manageAccountTitle);
this.channel_handle = new Text(data.channelHandle);
}
}
//# sourceMappingURL=ActiveAccountHeader.js.map
@@ -0,0 +1 @@
{"version":3,"file":"ActiveAccountHeader.js","sourceRoot":"","sources":["../../../../src/parser/classes/ActiveAccountHeader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC,OAAO,IAAI,MAAM,gBAAgB,CAAC;AAClC,OAAO,SAAS,MAAM,qBAAqB,CAAC;AAC5C,OAAO,kBAAkB,MAAM,yBAAyB,CAAC;AAEzD,MAAM,CAAC,OAAO,OAAO,mBAAoB,SAAQ,MAAM;IACrD,MAAM,CAAC,IAAI,GAAG,qBAAqB,CAAC;IAE7B,YAAY,CAAO;IACnB,aAAa,CAAc;IAC3B,QAAQ,CAAqB;IAC7B,oBAAoB,CAAO;IAC3B,cAAc,CAAO;IAE5B,YAAY,IAAa;QACvB,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,YAAY,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC/C,IAAI,CAAC,aAAa,GAAG,SAAS,CAAC,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC/D,IAAI,CAAC,QAAQ,GAAG,IAAI,kBAAkB,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QAC7D,IAAI,CAAC,oBAAoB,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;QAC9D,IAAI,CAAC,cAAc,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IACrD,CAAC"}
+11
View File
@@ -0,0 +1,11 @@
import { type ObservedArray, YTNode } from '../helpers.js';
import { type RawNode } from '../index.js';
import Button from './Button.js';
import MenuTitle from './MenuTitle.js';
import PlaylistAddToOption from './PlaylistAddToOption.js';
export default class AddToPlaylist extends YTNode {
static type: string;
actions: ObservedArray<MenuTitle | Button>;
playlists: ObservedArray<PlaylistAddToOption>;
constructor(data: RawNode);
}
+16
View File
@@ -0,0 +1,16 @@
import { YTNode } from '../helpers.js';
import { Parser } from '../index.js';
import Button from './Button.js';
import MenuTitle from './MenuTitle.js';
import PlaylistAddToOption from './PlaylistAddToOption.js';
export default class AddToPlaylist extends YTNode {
static type = 'AddToPlaylist';
actions;
playlists;
constructor(data) {
super();
this.actions = Parser.parseArray(data.actions, [MenuTitle, Button]);
this.playlists = Parser.parseArray(data.playlists, PlaylistAddToOption);
}
}
//# sourceMappingURL=AddToPlaylist.js.map
@@ -0,0 +1 @@
{"version":3,"file":"AddToPlaylist.js","sourceRoot":"","sources":["../../../../src/parser/classes/AddToPlaylist.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,MAAM,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,MAAM,EAAgB,MAAM,aAAa,CAAC;AACnD,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,mBAAmB,MAAM,0BAA0B,CAAC;AAE3D,MAAM,CAAC,OAAO,OAAO,aAAc,SAAQ,MAAM;IAC/C,MAAM,CAAC,IAAI,GAAG,eAAe,CAAC;IAEvB,OAAO,CAAoC;IAC3C,SAAS,CAAqC;IAErD,YAAY,IAAa;QACvB,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,CAAE,SAAS,EAAE,MAAM,CAAE,CAAC,CAAC;QACtE,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,EAAE,mBAAmB,CAAC,CAAC;IAC1E,CAAC"}
+10
View File
@@ -0,0 +1,10 @@
import Text from './misc/Text.js';
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
export type AlertType = 'UNKNOWN' | 'WARNING' | 'ERROR' | 'SUCCESS' | 'INFO';
export default class Alert extends YTNode {
static type: string;
text: Text;
alert_type: AlertType;
constructor(data: RawNode);
}
+13
View File
@@ -0,0 +1,13 @@
import Text from './misc/Text.js';
import { YTNode } from '../helpers.js';
export default class Alert extends YTNode {
static type = 'Alert';
text;
alert_type;
constructor(data) {
super();
this.text = new Text(data.text);
this.alert_type = data.type;
}
}
//# sourceMappingURL=Alert.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"Alert.js","sourceRoot":"","sources":["../../../../src/parser/classes/Alert.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,gBAAgB,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAKvC,MAAM,CAAC,OAAO,OAAO,KAAM,SAAQ,MAAM;IACvC,MAAM,CAAC,IAAI,GAAG,OAAO,CAAC;IAEtB,IAAI,CAAO;IACX,UAAU,CAAY;IAEtB,YAAY,IAAa;QACvB,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC;IAC9B,CAAC"}
+11
View File
@@ -0,0 +1,11 @@
import Button from './Button.js';
import Text from './misc/Text.js';
import { YTNode } from '../helpers.js';
import { type RawNode } from '../index.js';
export default class AlertWithButton extends YTNode {
static type: string;
text: Text;
alert_type: string;
dismiss_button: Button | null;
constructor(data: RawNode);
}
+17
View File
@@ -0,0 +1,17 @@
import Button from './Button.js';
import Text from './misc/Text.js';
import { YTNode } from '../helpers.js';
import { Parser } from '../index.js';
export default class AlertWithButton extends YTNode {
static type = 'AlertWithButton';
text;
alert_type;
dismiss_button;
constructor(data) {
super();
this.text = new Text(data.text);
this.alert_type = data.type;
this.dismiss_button = Parser.parseItem(data.dismissButton, Button);
}
}
//# sourceMappingURL=AlertWithButton.js.map
@@ -0,0 +1 @@
{"version":3,"file":"AlertWithButton.js","sourceRoot":"","sources":["../../../../src/parser/classes/AlertWithButton.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,IAAI,MAAM,gBAAgB,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,MAAM,EAAgB,MAAM,aAAa,CAAC;AAEnD,MAAM,CAAC,OAAO,OAAO,eAAgB,SAAQ,MAAM;IACjD,MAAM,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAEhC,IAAI,CAAO;IACX,UAAU,CAAS;IACnB,cAAc,CAAgB;IAE9B,YAAY,IAAa;QACvB,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC;QAC5B,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;IACrE,CAAC"}
@@ -0,0 +1,8 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../types/index.js';
import Thumbnail from './misc/Thumbnail.js';
export default class AnimatedThumbnailOverlayView extends YTNode {
static type: string;
thumbnail: Thumbnail[];
constructor(data: RawNode);
}
@@ -0,0 +1,11 @@
import { YTNode } from '../helpers.js';
import Thumbnail from './misc/Thumbnail.js';
export default class AnimatedThumbnailOverlayView extends YTNode {
static type = 'AnimatedThumbnailOverlayView';
thumbnail;
constructor(data) {
super();
this.thumbnail = Thumbnail.fromResponse(data.thumbnail);
}
}
//# sourceMappingURL=AnimatedThumbnailOverlayView.js.map
@@ -0,0 +1 @@
{"version":3,"file":"AnimatedThumbnailOverlayView.js","sourceRoot":"","sources":["../../../../src/parser/classes/AnimatedThumbnailOverlayView.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC,OAAO,SAAS,MAAM,qBAAqB,CAAC;AAE5C,MAAM,CAAC,OAAO,OAAO,4BAA6B,SAAQ,MAAM;IAC9D,MAAM,CAAC,IAAI,GAAG,8BAA8B,CAAC;IAEtC,SAAS,CAAc;IAE9B,YAAY,IAAa;QACvB,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC1D,CAAC"}
@@ -0,0 +1,9 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import Text from './misc/Text.js';
export default class AttributionView extends YTNode {
static type: string;
text: Text;
suffix: Text;
constructor(data: RawNode);
}
+13
View File
@@ -0,0 +1,13 @@
import { YTNode } from '../helpers.js';
import Text from './misc/Text.js';
export default class AttributionView extends YTNode {
static type = 'AttributionView';
text;
suffix;
constructor(data) {
super();
this.text = Text.fromAttributed(data.text);
this.suffix = Text.fromAttributed(data.suffix);
}
}
//# sourceMappingURL=AttributionView.js.map
@@ -0,0 +1 @@
{"version":3,"file":"AttributionView.js","sourceRoot":"","sources":["../../../../src/parser/classes/AttributionView.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC,OAAO,IAAI,MAAM,gBAAgB,CAAC;AAElC,MAAM,CAAC,OAAO,OAAO,eAAgB,SAAQ,MAAM;IACjD,MAAM,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAEhC,IAAI,CAAO;IACX,MAAM,CAAO;IAEb,YAAY,IAAa;QACvB,KAAK,EAAE,CAAC;QAER,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACjD,CAAC"}
@@ -0,0 +1,7 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
export default class AudioOnlyPlayability extends YTNode {
static type: string;
audio_only_availability: string;
constructor(data: RawNode);
}
@@ -0,0 +1,10 @@
import { YTNode } from '../helpers.js';
export default class AudioOnlyPlayability extends YTNode {
static type = 'AudioOnlyPlayability';
audio_only_availability;
constructor(data) {
super();
this.audio_only_availability = data.audioOnlyAvailability;
}
}
//# sourceMappingURL=AudioOnlyPlayability.js.map
@@ -0,0 +1 @@
{"version":3,"file":"AudioOnlyPlayability.js","sourceRoot":"","sources":["../../../../src/parser/classes/AudioOnlyPlayability.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAGvC,MAAM,CAAC,OAAO,OAAO,oBAAqB,SAAQ,MAAM;IACtD,MAAM,CAAC,IAAI,GAAG,sBAAsB,CAAC;IAErC,uBAAuB,CAAS;IAEhC,YAAa,IAAa;QACxB,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,uBAAuB,GAAG,IAAI,CAAC,qBAAqB,CAAC;IAC5D,CAAC"}
@@ -0,0 +1,10 @@
import { YTNode } from '../helpers.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import type { RawNode } from '../index.js';
export default class AutomixPreviewVideo extends YTNode {
static type: string;
playlist_video?: {
endpoint: NavigationEndpoint;
};
constructor(data: RawNode);
}

Some files were not shown because too many files have changed in this diff Show More