mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-26 16:18:51 +00:00
refactor!: overhaul core classes and remove redundant code (#388)
* feat(Player.ts): append `cver` to deciphered URLs * refactor(Actions.ts): remove redundant `getVideoInfo` function This is leftover code from previous versions. It had many problems and it is no longer required. * fix(Kids.ts): remove unneeded `await` keywords * dev: add more endpoints * chore: update deps * refactor: separate endpoints into files * dev: improve types * dev: add more endpoints * refactor: put clients in a separate directory inside `core` * chore: lint * refactor: move mixins and managers to separate folders * chore: fix tests * dev: add `CreateVideoEndpoint` * chore: clean up * chore: lint * chore: add some comments * chore: remove unnecessary test * dev: add `playlist/CreateEndpoint` * dev: add `playlist/DeleteEndpoint` * dev: add `browse/EditPlaylistEndpoint` * fix(parser): add a few checks to avoid parsing errors
This commit is contained in:
120
src/core/managers/AccountManager.ts
Normal file
120
src/core/managers/AccountManager.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import AccountInfo from '../../parser/youtube/AccountInfo.js';
|
||||
import Analytics from '../../parser/youtube/Analytics.js';
|
||||
import Settings from '../../parser/youtube/Settings.js';
|
||||
import TimeWatched from '../../parser/youtube/TimeWatched.js';
|
||||
|
||||
import Proto from '../../proto/index.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
import { Account, BrowseEndpoint, Channel } from '../endpoints/index.js';
|
||||
|
||||
import type Actions from '../Actions.js';
|
||||
import type { ApiResponse } from '../Actions.js';
|
||||
|
||||
export default class AccountManager {
|
||||
#actions: Actions;
|
||||
|
||||
channel: {
|
||||
editName: (new_name: string) => Promise<ApiResponse>;
|
||||
editDescription: (new_description: string) => Promise<ApiResponse>;
|
||||
getBasicAnalytics: () => Promise<Analytics>;
|
||||
};
|
||||
|
||||
constructor(actions: Actions) {
|
||||
this.#actions = actions;
|
||||
|
||||
this.channel = {
|
||||
/**
|
||||
* Edits channel name.
|
||||
* @param new_name - The new channel name.
|
||||
*/
|
||||
editName: (new_name: string) => {
|
||||
if (!this.#actions.session.logged_in)
|
||||
throw new InnertubeError('You must be signed in to perform this operation.');
|
||||
|
||||
return this.#actions.execute(
|
||||
Channel.EditNameEndpoint.PATH,
|
||||
Channel.EditNameEndpoint.build({
|
||||
given_name: new_name
|
||||
})
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Edits channel description.
|
||||
* @param new_description - The new description.
|
||||
*/
|
||||
editDescription: (new_description: string) => {
|
||||
if (!this.#actions.session.logged_in)
|
||||
throw new InnertubeError('You must be signed in to perform this operation.');
|
||||
|
||||
return this.#actions.execute(
|
||||
Channel.EditDescriptionEndpoint.PATH,
|
||||
Channel.EditDescriptionEndpoint.build({
|
||||
given_description: new_description
|
||||
})
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Retrieves basic channel analytics.
|
||||
*/
|
||||
getBasicAnalytics: () => this.getAnalytics()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves channel info.
|
||||
*/
|
||||
async getInfo(): Promise<AccountInfo> {
|
||||
if (!this.#actions.session.logged_in)
|
||||
throw new InnertubeError('You must be signed in to perform this operation.');
|
||||
|
||||
const response = await this.#actions.execute(
|
||||
Account.AccountListEndpoint.PATH,
|
||||
Account.AccountListEndpoint.build()
|
||||
);
|
||||
|
||||
return new AccountInfo(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves time watched statistics.
|
||||
*/
|
||||
async getTimeWatched(): Promise<TimeWatched> {
|
||||
const response = await this.#actions.execute(
|
||||
BrowseEndpoint.PATH, BrowseEndpoint.build({
|
||||
browse_id: 'SPtime_watched',
|
||||
client: 'ANDROID'
|
||||
})
|
||||
);
|
||||
|
||||
return new TimeWatched(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens YouTube settings.
|
||||
*/
|
||||
async getSettings(): Promise<Settings> {
|
||||
const response = await this.#actions.execute(
|
||||
BrowseEndpoint.PATH, BrowseEndpoint.build({
|
||||
browse_id: 'SPaccount_overview'
|
||||
})
|
||||
);
|
||||
return new Settings(this.#actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves basic channel analytics.
|
||||
*/
|
||||
async getAnalytics(): Promise<Analytics> {
|
||||
const info = await this.getInfo();
|
||||
|
||||
const response = await this.#actions.execute(
|
||||
BrowseEndpoint.PATH, BrowseEndpoint.build({
|
||||
browse_id: 'FEanalytics_screen',
|
||||
params: Proto.encodeChannelAnalyticsParams(info.footers?.endpoint.payload.browseId),
|
||||
client: 'ANDROID'
|
||||
})
|
||||
);
|
||||
|
||||
return new Analytics(response);
|
||||
}
|
||||
}
|
||||
200
src/core/managers/InteractionManager.ts
Normal file
200
src/core/managers/InteractionManager.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import Proto from '../../proto/index.js';
|
||||
import type Actions from '../Actions.js';
|
||||
import type { ApiResponse } from '../Actions.js';
|
||||
|
||||
import { throwIfMissing } from '../../utils/Utils.js';
|
||||
import { LikeEndpoint, DislikeEndpoint, RemoveLikeEndpoint } from '../endpoints/like/index.js';
|
||||
import { SubscribeEndpoint, UnsubscribeEndpoint } from '../endpoints/subscription/index.js';
|
||||
import { CreateCommentEndpoint, PerformCommentActionEndpoint } from '../endpoints/comment/index.js';
|
||||
import { ModifyChannelPreferenceEndpoint } from '../endpoints/notification/index.js';
|
||||
|
||||
export default class InteractionManager {
|
||||
#actions: Actions;
|
||||
|
||||
constructor(actions: Actions) {
|
||||
this.#actions = actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Likes a given video.
|
||||
* @param video_id - The video ID
|
||||
*/
|
||||
async like(video_id: string): Promise<ApiResponse> {
|
||||
throwIfMissing({ video_id });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
throw new Error('You must be signed in to perform this operation.');
|
||||
|
||||
const action = await this.#actions.execute(
|
||||
LikeEndpoint.PATH, LikeEndpoint.build({
|
||||
client: 'ANDROID',
|
||||
target: { video_id }
|
||||
})
|
||||
);
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dislikes a given video.
|
||||
* @param video_id - The video ID
|
||||
*/
|
||||
async dislike(video_id: string): Promise<ApiResponse> {
|
||||
throwIfMissing({ video_id });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
throw new Error('You must be signed in to perform this operation.');
|
||||
|
||||
const action = await this.#actions.execute(
|
||||
DislikeEndpoint.PATH, DislikeEndpoint.build({
|
||||
client: 'ANDROID',
|
||||
target: { video_id }
|
||||
})
|
||||
);
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a like/dislike.
|
||||
* @param video_id - The video ID
|
||||
*/
|
||||
async removeRating(video_id: string): Promise<ApiResponse> {
|
||||
throwIfMissing({ video_id });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
throw new Error('You must be signed in to perform this operation.');
|
||||
|
||||
const action = await this.#actions.execute(
|
||||
RemoveLikeEndpoint.PATH, RemoveLikeEndpoint.build({
|
||||
client: 'ANDROID',
|
||||
target: { video_id }
|
||||
})
|
||||
);
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to a given channel.
|
||||
* @param channel_id - The channel ID
|
||||
*/
|
||||
async subscribe(channel_id: string): Promise<ApiResponse> {
|
||||
throwIfMissing({ channel_id });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
throw new Error('You must be signed in to perform this operation.');
|
||||
|
||||
const action = await this.#actions.execute(
|
||||
SubscribeEndpoint.PATH, SubscribeEndpoint.build({
|
||||
client: 'ANDROID',
|
||||
channel_ids: [ channel_id ],
|
||||
params: 'EgIIAhgA'
|
||||
})
|
||||
);
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes from a given channel.
|
||||
* @param channel_id - The channel ID
|
||||
*/
|
||||
async unsubscribe(channel_id: string): Promise<ApiResponse> {
|
||||
throwIfMissing({ channel_id });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
throw new Error('You must be signed in to perform this operation.');
|
||||
|
||||
const action = await this.#actions.execute(
|
||||
UnsubscribeEndpoint.PATH, UnsubscribeEndpoint.build({
|
||||
client: 'ANDROID',
|
||||
channel_ids: [ channel_id ],
|
||||
params: 'CgIIAhgA'
|
||||
})
|
||||
);
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts a comment on a given video.
|
||||
* @param video_id - The video ID
|
||||
* @param text - The comment text
|
||||
*/
|
||||
async comment(video_id: string, text: string): Promise<ApiResponse> {
|
||||
throwIfMissing({ video_id, text });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
throw new Error('You must be signed in to perform this operation.');
|
||||
|
||||
const action = await this.#actions.execute(
|
||||
CreateCommentEndpoint.PATH, CreateCommentEndpoint.build({
|
||||
comment_text: text,
|
||||
create_comment_params: Proto.encodeCommentParams(video_id),
|
||||
client: 'ANDROID'
|
||||
})
|
||||
);
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a given text using YouTube's comment translate feature.
|
||||
*
|
||||
* @param target_language - an ISO language code
|
||||
* @param args - optional arguments
|
||||
*/
|
||||
async translate(text: string, target_language: string, args: { video_id?: string; comment_id?: string; } = {}) {
|
||||
throwIfMissing({ text, target_language });
|
||||
|
||||
const target_action = Proto.encodeCommentActionParams(22, { text, target_language, ...args });
|
||||
|
||||
const response = await this.#actions.execute(
|
||||
PerformCommentActionEndpoint.PATH, PerformCommentActionEndpoint.build({
|
||||
client: 'ANDROID',
|
||||
actions: [ target_action ]
|
||||
})
|
||||
);
|
||||
|
||||
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: string, type: 'PERSONALIZED' | 'ALL' | 'NONE'): Promise<ApiResponse> {
|
||||
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 action = await this.#actions.execute(
|
||||
ModifyChannelPreferenceEndpoint.PATH, ModifyChannelPreferenceEndpoint.build({
|
||||
client: 'WEB',
|
||||
params: Proto.encodeNotificationPref(channel_id, pref_types[type.toUpperCase() as keyof typeof pref_types])
|
||||
})
|
||||
);
|
||||
|
||||
return action;
|
||||
}
|
||||
}
|
||||
203
src/core/managers/PlaylistManager.ts
Normal file
203
src/core/managers/PlaylistManager.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import Playlist from '../../parser/youtube/Playlist.js';
|
||||
import type Actions from '../Actions.js';
|
||||
import type Feed from '../mixins/Feed.js';
|
||||
|
||||
import type { EditPlaylistEndpointOptions } from '../../types/index.js';
|
||||
import { InnertubeError, throwIfMissing } from '../../utils/Utils.js';
|
||||
import { EditPlaylistEndpoint } from '../endpoints/browse/index.js';
|
||||
import { BrowseEndpoint } from '../endpoints/index.js';
|
||||
import { CreateEndpoint, DeleteEndpoint } from '../endpoints/playlist/index.js';
|
||||
|
||||
export default class PlaylistManager {
|
||||
#actions: Actions;
|
||||
|
||||
constructor(actions: 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: string, video_ids: string[]): Promise<{ success: boolean; status_code: number; playlist_id?: string; data: any }> {
|
||||
throwIfMissing({ title, video_ids });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
throw new InnertubeError('You must be signed in to perform this operation.');
|
||||
|
||||
const response = await this.#actions.execute(
|
||||
CreateEndpoint.PATH, CreateEndpoint.build({
|
||||
ids: video_ids,
|
||||
title
|
||||
})
|
||||
);
|
||||
|
||||
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: string): Promise<{ playlist_id: string; success: boolean; status_code: number; data: any }> {
|
||||
throwIfMissing({ playlist_id });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
throw new InnertubeError('You must be signed in to perform this operation.');
|
||||
|
||||
const response = await this.#actions.execute(
|
||||
DeleteEndpoint.PATH, DeleteEndpoint.build({
|
||||
playlist_id
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
playlist_id,
|
||||
success: response.success,
|
||||
status_code: response.status_code,
|
||||
data: response.data
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: string, video_ids: string[]): Promise<{ playlist_id: string; action_result: any }> {
|
||||
throwIfMissing({ playlist_id, video_ids });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
throw new InnertubeError('You must be signed in to perform this operation.');
|
||||
|
||||
const response = await this.#actions.execute(
|
||||
EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build({
|
||||
actions: video_ids.map((id) => ({
|
||||
action: 'ACTION_ADD_VIDEO',
|
||||
added_video_id: id
|
||||
})),
|
||||
playlist_id
|
||||
})
|
||||
);
|
||||
|
||||
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.
|
||||
*/
|
||||
async removeVideos(playlist_id: string, video_ids: string[]): Promise<{ playlist_id: string; action_result: any }> {
|
||||
throwIfMissing({ playlist_id, video_ids });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
throw new InnertubeError('You must be signed in to perform this operation.');
|
||||
|
||||
const info = await this.#actions.execute(
|
||||
BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: `VL${playlist_id}` }), parse: true }
|
||||
);
|
||||
|
||||
const playlist = new Playlist(this.#actions, info, true);
|
||||
|
||||
if (!playlist.info.is_editable)
|
||||
throw new InnertubeError('This playlist cannot be edited.', playlist_id);
|
||||
|
||||
const payload: EditPlaylistEndpointOptions = { playlist_id, actions: [] };
|
||||
|
||||
const getSetVideoIds = async (pl: Feed): Promise<void> => {
|
||||
const videos = pl.videos.filter((video) => video_ids.includes(video.key('id').string()));
|
||||
|
||||
videos.forEach((video) =>
|
||||
payload.actions.push({
|
||||
action: 'ACTION_REMOVE_VIDEO',
|
||||
set_video_id: 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 response = await this.#actions.execute(
|
||||
EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build(payload)
|
||||
);
|
||||
|
||||
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: string, moved_video_id: string, predecessor_video_id: string): Promise<{ playlist_id: string; action_result: any; }> {
|
||||
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 info = await this.#actions.execute(
|
||||
BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: `VL${playlist_id}` }), parse: true }
|
||||
);
|
||||
|
||||
const playlist = new Playlist(this.#actions, info, true);
|
||||
|
||||
if (!playlist.info.is_editable)
|
||||
throw new InnertubeError('This playlist cannot be edited.', playlist_id);
|
||||
|
||||
const payload: EditPlaylistEndpointOptions = { playlist_id, actions: [] };
|
||||
|
||||
let set_video_id_0: string | undefined, set_video_id_1: string | undefined;
|
||||
|
||||
const getSetVideoIds = async (pl: Feed): Promise<void> => {
|
||||
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',
|
||||
set_video_id: set_video_id_0,
|
||||
moved_set_video_id_predecessor: set_video_id_1
|
||||
});
|
||||
|
||||
const response = await this.#actions.execute(
|
||||
EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build(payload)
|
||||
);
|
||||
|
||||
return {
|
||||
playlist_id,
|
||||
action_result: response.data.actions // TODO: implement actions in the parser
|
||||
};
|
||||
}
|
||||
}
|
||||
3
src/core/managers/index.ts
Normal file
3
src/core/managers/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as AccountManager } from './AccountManager.js';
|
||||
export { default as PlaylistManager } from './PlaylistManager.js';
|
||||
export { default as InteractionManager } from './InteractionManager.js';
|
||||
Reference in New Issue
Block a user