UI/UX: Adiciona campo opcional para colagem manual de transcrição no VideoMind
This commit is contained in:
+122
@@ -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
@@ -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
|
||||
+1
File diff suppressed because one or more lines are too long
+45
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"Actions.js","sourceRoot":"","sources":["../../../src/core/Actions.ts"],"names":[],"mappings":"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"}
|
||||
+49
@@ -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
@@ -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
|
||||
+1
File diff suppressed because one or more lines are too long
+25
@@ -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
@@ -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
|
||||
+1
File diff suppressed because one or more lines are too long
+277
@@ -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
@@ -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
|
||||
+1
File diff suppressed because one or more lines are too long
+18
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"Kids.js","sourceRoot":"","sources":["../../../../src/core/clients/Kids.ts"],"names":[],"mappings":"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"}
|
||||
+34
@@ -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
@@ -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
|
||||
+1
File diff suppressed because one or more lines are too long
+34
@@ -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
@@ -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
|
||||
+1
File diff suppressed because one or more lines are too long
+3
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
+1
@@ -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"}
|
||||
+58
@@ -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
@@ -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
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"InteractionManager.js","sourceRoot":"","sources":["../../../../src/core/managers/InteractionManager.ts"],"names":[],"mappings":"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"}
|
||||
+83
@@ -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
@@ -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
|
||||
+1
File diff suppressed because one or more lines are too long
+3
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
+1
File diff suppressed because one or more lines are too long
+21
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
+1
File diff suppressed because one or more lines are too long
+12
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"AboutChannelView.js","sourceRoot":"","sources":["../../../../src/parser/classes/AboutChannelView.ts"],"names":[],"mappings":"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"}
|
||||
+10
@@ -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
@@ -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
|
||||
+1
@@ -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
@@ -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
@@ -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
@@ -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"}
|
||||
+11
@@ -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
@@ -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
|
||||
+1
@@ -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"}
|
||||
+8
@@ -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);
|
||||
}
|
||||
+11
@@ -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
|
||||
+1
@@ -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"}
|
||||
+11
@@ -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
@@ -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
|
||||
+1
@@ -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"}
|
||||
+14
@@ -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);
|
||||
}
|
||||
+21
@@ -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
|
||||
+1
@@ -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
@@ -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
@@ -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
|
||||
+1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
+1
@@ -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"}
|
||||
Generated
Vendored
+8
@@ -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);
|
||||
}
|
||||
+11
@@ -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
|
||||
Generated
Vendored
+1
@@ -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"}
|
||||
+9
@@ -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
@@ -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
|
||||
+1
@@ -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"}
|
||||
+7
@@ -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);
|
||||
}
|
||||
+10
@@ -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
|
||||
+1
@@ -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"}
|
||||
+10
@@ -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
Reference in New Issue
Block a user