mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-28 09:06:51 +00:00
refactor!: welp, a lot of stuff
- Use the OS temp folder to cache the player, closes #57. - Added support for editing channel name, closes #40. - Added support for editing channel description. - Added support for retrieving basic channel analytics, closes #54. - Moved `Innertube#getAccountInfo()` to `Innertube#account`, and renamed it to `getInfo()`. - `getInfo()` is now able to return email, channel id, etc. - Improved jsdoc.
This commit is contained in:
251
lib/core/AccountManager.js
Normal file
251
lib/core/AccountManager.js
Normal file
@@ -0,0 +1,251 @@
|
||||
'use strict';
|
||||
|
||||
const Utils = require('../utils/Utils');
|
||||
const Constants = require('../utils/Constants');
|
||||
const Proto = require('../proto');
|
||||
|
||||
/** @namespace */
|
||||
class AccountManager {
|
||||
#actions;
|
||||
|
||||
/**
|
||||
* @param {Actions} actions
|
||||
* @constructor
|
||||
*/
|
||||
constructor (actions) {
|
||||
this.#actions = actions;
|
||||
|
||||
/** @namespace */
|
||||
this.channel = {
|
||||
/**
|
||||
* Edits channel name.
|
||||
*
|
||||
* @param {string} new_name
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
editName: (new_name) => this.#actions.channel('channel/edit_name', { new_name }),
|
||||
|
||||
/**
|
||||
* Edits channel description.
|
||||
*
|
||||
* @param {string} new_description
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
editDescription: (new_description) => this.#actions.channel('channel/edit_description', { new_description }),
|
||||
|
||||
/**
|
||||
* Retrieves basic channel analytics.
|
||||
* @borrows AccountManager#getAnalytics as getBasicAnalytics
|
||||
*/
|
||||
getBasicAnalytics: () => this.getAnalytics()
|
||||
}
|
||||
|
||||
/** @namespace */
|
||||
this.settings = {
|
||||
notifications: {
|
||||
/**
|
||||
* Notify about activity from the channels you're subscribed to.
|
||||
*
|
||||
* @param {boolean} option - ON | OFF
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
setSubscriptions: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SUBSCRIPTIONS, 'SPaccount_notifications', option),
|
||||
|
||||
/**
|
||||
* Recommended content notifications.
|
||||
*
|
||||
* @param {boolean} option - ON | OFF
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
setRecommendedVideos: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.RECOMMENDED_VIDEOS, 'SPaccount_notifications', option),
|
||||
|
||||
/**
|
||||
* Notify about activity on your channel.
|
||||
*
|
||||
* @param {boolean} option - ON | OFF
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
setChannelActivity: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.CHANNEL_ACTIVITY, 'SPaccount_notifications', option),
|
||||
|
||||
/**
|
||||
* Notify about replies to your comments.
|
||||
*
|
||||
* @param {boolean} option - ON | OFF
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
setCommentReplies: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.COMMENT_REPLIES, 'SPaccount_notifications', option),
|
||||
|
||||
/**
|
||||
* Notify when others mention your channel.
|
||||
*
|
||||
* @param {boolean} option - ON | OFF
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
setMentions: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.USER_MENTION, 'SPaccount_notifications', option),
|
||||
|
||||
/**
|
||||
* Notify when others share your content on their channels.
|
||||
*
|
||||
* @param {boolean} option - ON | OFF
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
setSharedContent: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SHARED_CONTENT, 'SPaccount_notifications', option)
|
||||
},
|
||||
privacy: {
|
||||
/**
|
||||
* If set to true, your subscriptions won't be visible to others.
|
||||
*
|
||||
* @param {boolean} option - ON | OFF
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
setSubscriptionsPrivate: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SUBSCRIPTIONS_PRIVACY, 'SPaccount_privacy', option),
|
||||
|
||||
/**
|
||||
* If set to true, saved playlists won't appear on your channel.
|
||||
*
|
||||
* @param {boolean} option - ON | OFF
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
setSavedPlaylistsPrivate: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.PLAYLISTS_PRIVACY, 'SPaccount_privacy', option)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to perform changes on an account's settings.
|
||||
*
|
||||
* @param {string} setting_id
|
||||
* @param {string} type
|
||||
* @param {string} new_value
|
||||
* @private
|
||||
*
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
async #setSetting(setting_id, type, new_value) {
|
||||
Utils.throwIfMissing({ setting_id, type, new_value });
|
||||
|
||||
const values = { ON: true, OFF: false };
|
||||
|
||||
if (!values.hasOwnProperty(new_value))
|
||||
throw new Utils.InnertubeError('Invalid option', { option: new_value, available_options: Object.keys(values) });
|
||||
|
||||
const response = await this.#actions.browse(type);
|
||||
|
||||
const contents = ({
|
||||
SPaccount_notifications: () => Utils.findNode(response.data, 'contents', 'Your preferences', 13, false).options,
|
||||
SPaccount_privacy: () => Utils.findNode(response.data, 'contents', 'settingsSwitchRenderer', 13, false).options
|
||||
})[type.trim()]();
|
||||
|
||||
const option = contents.find((option) => option.settingsSwitchRenderer.enableServiceEndpoint.setSettingEndpoint.settingItemIdForClient == setting_id);
|
||||
|
||||
const setting_item_id = option.settingsSwitchRenderer.enableServiceEndpoint.setSettingEndpoint.settingItemId;
|
||||
const set_setting = await this.#actions.account('account/set_setting', {
|
||||
new_value: type == 'SPaccount_privacy' ? !values[new_value] : values[new_value],
|
||||
setting_item_id
|
||||
});
|
||||
|
||||
return set_setting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves channel info.
|
||||
* @returns {Promise.<{ name: string; email: string; channel_id: string; subscriber_count: string; photo: object[]; }>}
|
||||
*/
|
||||
async getInfo() {
|
||||
const response = await this.#actions.account('account/accounts_list', { client: 'ANDROID' });
|
||||
|
||||
const account_item_section_renderer = Utils.findNode(response.data, 'contents', 'accountItem', 8, false);
|
||||
const profile = account_item_section_renderer.accountItem.serviceEndpoint.signInEndpoint.directSigninUserProfile;
|
||||
|
||||
const name = profile.accountName;
|
||||
const email = profile.email;
|
||||
const photo = profile.accountPhoto.thumbnails;
|
||||
const subscriber_count = account_item_section_renderer.accountItem.accountByline.runs.map((run) => run.text).join('');
|
||||
const channel_id = response.data.contents[0].accountSectionListRenderer.footers[0].accountChannelRenderer.navigationEndpoint.browseEndpoint.browseId;
|
||||
|
||||
return { name, email, channel_id, subscriber_count, photo };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves time watched statistics.
|
||||
* @returns {Promise.<[{ title: string; time: string; }]>}
|
||||
*/
|
||||
async getTimeWatched() {
|
||||
const response = await this.#actions.browse('SPtime_watched', { client: 'ANDROID' });
|
||||
|
||||
const rows = Utils.findNode(response.data, 'contents', 'statRowRenderer', 11, false);
|
||||
|
||||
const stats = rows.map((row) => {
|
||||
const renderer = row.statRowRenderer;
|
||||
if (renderer) {
|
||||
return {
|
||||
title: renderer.title.runs.map((run) => run.text).join(''),
|
||||
time: renderer.contents.runs.map((run) => run.text).join('')
|
||||
}
|
||||
}
|
||||
}).filter((stat) => stat);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves basic channel analytics.
|
||||
*
|
||||
* @returns {Promise.<{ metrics: { title: string; subtitle: string; metric_value: string;
|
||||
* comparison_indicator: object; series_configuration: object; }[]; top_content: { views: string;
|
||||
* published: string; thumbnails: object[]; duration: string; is_short: boolean }[]; }>}
|
||||
*/
|
||||
async getAnalytics() {
|
||||
const info = await this.getInfo();
|
||||
|
||||
const params = Proto.encodeChannelAnalyticsParams(info.channel_id);
|
||||
const action = await this.#actions.browse('FEanalytics_screen', { params, client: 'ANDROID' });
|
||||
|
||||
const contents = Utils.findNode(action.data, 'contents', 'elementRenderer', 11, false);
|
||||
|
||||
const analytics = {
|
||||
metrics: {},
|
||||
top_content: {}
|
||||
}
|
||||
|
||||
contents.forEach((el) => {
|
||||
const element = el.elementRenderer.newElement;
|
||||
const model = element.type.componentType.model;
|
||||
const key = Object.keys(model)[0];
|
||||
|
||||
switch (key) {
|
||||
case 'analyticsRootModel':
|
||||
const sections = model.analyticsRootModel.analyticsKeyMetricsData.dataModel.sections;
|
||||
|
||||
analytics.metrics = sections.map((section) => ({
|
||||
title: section.title,
|
||||
subtitle: section.subtitle,
|
||||
metric_value: section.metricValue,
|
||||
comparison_indicator: section.comparisonIndicator,
|
||||
series_configuration: section.seriesConfiguration
|
||||
}));
|
||||
break;
|
||||
case 'analyticsVodCarouselCardModel':
|
||||
const video_carousel = model.analyticsVodCarouselCardModel.videoCarouselData;
|
||||
|
||||
analytics.top_content = video_carousel?.videos.map((video) => ({
|
||||
title: video.videoTitle,
|
||||
metadata: {
|
||||
views: video.videoDescription.split('·')[0].trim(),
|
||||
published: video.videoDescription.split('·')[1].trim(),
|
||||
thumbnails: video.thumbnailDetails.thumbnails,
|
||||
duration: video.formattedLength,
|
||||
is_short: video.isShort
|
||||
}
|
||||
})) || [];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return analytics;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AccountManager;
|
||||
134
lib/core/InteractionManager.js
Normal file
134
lib/core/InteractionManager.js
Normal file
@@ -0,0 +1,134 @@
|
||||
'use strict';
|
||||
|
||||
const Utils = require('../utils/Utils');
|
||||
|
||||
/** @namespace */
|
||||
class InteractionManager {
|
||||
#actions;
|
||||
|
||||
/**
|
||||
* @param {Actions} actions
|
||||
* @constructor
|
||||
*/
|
||||
constructor(actions) {
|
||||
this.#actions = actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Likes a given video.
|
||||
* @param {string} video_id
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
async like(video_id) {
|
||||
Utils.throwIfMissing({ video_id });
|
||||
const action = await this.#actions.engage('like/like', { video_id });
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dislikes a given video.
|
||||
* @param {string} video_id
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
async dislike(video_id) {
|
||||
Utils.throwIfMissing({ video_id });
|
||||
const action = await this.#actions.engage('like/dislike', { video_id });
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a like/dislike.
|
||||
* @param {string} video_id
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
async removeLike(video_id) {
|
||||
Utils.throwIfMissing({ video_id });
|
||||
const action = await this.actions.engage('like/removelike', { video_id });
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to a given channel.
|
||||
* @param {string} channel_id
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
async subscribe(channel_id) {
|
||||
Utils.throwIfMissing({ channel_id });
|
||||
const action = await this.#actions.engage('subscription/subscribe', { channel_id });
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes from a given channel.
|
||||
* @param {string} channel_id
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
async unsubscribe(channel_id) {
|
||||
Utils.throwIfMissing({ channel_id });
|
||||
const action = await this.#actions.engage('subscription/unsubscribe', { channel_id });
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts a comment on a given video.
|
||||
*
|
||||
* @param {string} video_id
|
||||
* @param {string} text
|
||||
*
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
async comment(video_id, text) {
|
||||
Utils.throwIfMissing({ video_id, text });
|
||||
const action = await this.#actions.engage('comment/create_comment', { video_id, text });
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a given text using YouTube's comment translate feature.
|
||||
*
|
||||
* @param {string} text
|
||||
* @param {string} target_language - an ISO language code
|
||||
* @param {object} [args] - optional arguments
|
||||
* @param {string} [args.video_id]
|
||||
* @param {string} [args.comment_id]
|
||||
*
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; translated_content: string; data: object; }>}
|
||||
*/
|
||||
async translate(text, target_language, args = {}) {
|
||||
Utils.throwIfMissing({ text, target_language });
|
||||
|
||||
const response = await await this.#actions.engage('comment/perform_comment_action', {
|
||||
video_id: args.video_id,
|
||||
comment_id: args.comment_id,
|
||||
target_language: target_language,
|
||||
comment_action: 'translate',
|
||||
text
|
||||
});
|
||||
|
||||
const translated_content = Utils.findNode(response.data, 'frameworkUpdates', 'content', 7, false);
|
||||
|
||||
return {
|
||||
success: response.success,
|
||||
status_code: response.status_code,
|
||||
translated_content: translated_content.content,
|
||||
data: response.data
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes notification preferences for a given channel.
|
||||
* Only works with channels you are subscribed to.
|
||||
*
|
||||
* @param {string} channel_id
|
||||
* @param {string} type - `PERSONALIZED` | `ALL` | `NONE`
|
||||
*
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
async setNotificationPreferences(channel_id, type) {
|
||||
Utils.throwIfMissing({ channel_id, type });
|
||||
const action = await this.#actions.notifications('modify_channel_preference', { channel_id, pref: type || 'NONE' });
|
||||
return action;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InteractionManager;
|
||||
@@ -4,11 +4,8 @@ const Axios = require('axios');
|
||||
const Constants = require('../utils/Constants');
|
||||
const Uuid = require('uuid');
|
||||
|
||||
/** @namespace */
|
||||
class OAuth {
|
||||
#scope = Constants.OAUTH.SCOPE;
|
||||
#model_name = Constants.OAUTH.MODEL_NAME;
|
||||
#grant_type = Constants.OAUTH.GRANT_TYPE;
|
||||
|
||||
#oauth_code_url = `${Constants.URLS.YT_BASE}/o/oauth2/device/code`;
|
||||
#oauth_token_url = `${Constants.URLS.YT_BASE}/o/oauth2/token`;
|
||||
#oauth_revoke_url = `${Constants.URLS.YT_BASE}/o/oauth2/revoke`;
|
||||
@@ -17,6 +14,10 @@ class OAuth {
|
||||
#polling_interval = 5;
|
||||
#ev = null;
|
||||
|
||||
/**
|
||||
* @param {EventEmitter} ev
|
||||
* @constructor
|
||||
*/
|
||||
constructor(ev) {
|
||||
this.#ev = ev;
|
||||
}
|
||||
@@ -46,9 +47,9 @@ class OAuth {
|
||||
|
||||
const data = {
|
||||
client_id: this.client_id,
|
||||
scope: this.#scope,
|
||||
scope: Constants.OAUTH.SCOPE,
|
||||
device_id: Uuid.v4(),
|
||||
model_name: this.#model_name
|
||||
model_name: Constants.OAUTH.MODEL_NAME
|
||||
};
|
||||
|
||||
const response = await Axios.post(this.#oauth_code_url, JSON.stringify(data), Constants.OAUTH.HEADERS).catch((error) => error);
|
||||
@@ -77,7 +78,7 @@ class OAuth {
|
||||
client_id: this.client_id,
|
||||
client_secret: this.client_secret,
|
||||
code: device_code,
|
||||
grant_type: this.#grant_type
|
||||
grant_type: Constants.OAUTH.GRANT_TYPE
|
||||
};
|
||||
|
||||
setTimeout(async () => {
|
||||
@@ -204,16 +205,24 @@ class OAuth {
|
||||
return client_identity.groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the access token.
|
||||
* @returns {string}
|
||||
*/
|
||||
getAccessToken() {
|
||||
return this.#auth_info.access_token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the refresh token.
|
||||
* @returns {string}
|
||||
*/
|
||||
getRefreshToken() {
|
||||
return this.#auth_info.refresh_token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the auth info is valid.
|
||||
* Checks if the auth info format is valid.
|
||||
* @returns {boolean} true | false
|
||||
*/
|
||||
isValidAuthInfo() {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
const os = require('os');
|
||||
const Fs = require('fs');
|
||||
const Axios = require('axios');
|
||||
const Utils = require('../utils/Utils');
|
||||
const Constants = require('../utils/Constants');
|
||||
|
||||
/** @namespace */
|
||||
class Player {
|
||||
#player_id;
|
||||
#player_url;
|
||||
@@ -13,12 +15,16 @@ class Player {
|
||||
#ntoken_decipher_sc;
|
||||
#signature_decipher_sc;
|
||||
#signature_timestamp;
|
||||
|
||||
#cache_dir;
|
||||
|
||||
/**
|
||||
* Represents the YouTube Web player script.
|
||||
* @param {string} id - the id of the player.
|
||||
* @constructor
|
||||
*/
|
||||
constructor(id) {
|
||||
this.#player_id = id;
|
||||
this.#cache_dir = __dirname.slice(0, -8) + 'cache';
|
||||
this.#cache_dir = `${os.tmpdir()}/cache`;
|
||||
this.#player_url = Constants.URLS.YT_BASE + '/s/player/' + this.#player_id + '/player_ias.vflset/en_US/base.js';
|
||||
this.#player_path = `${this.#cache_dir}/${this.#player_id}.js`;
|
||||
}
|
||||
@@ -26,7 +32,6 @@ class Player {
|
||||
async init() {
|
||||
if (this.isCached()) {
|
||||
const player_data = Fs.readFileSync(this.#player_path).toString();
|
||||
|
||||
this.#signature_timestamp = this.#extractSigTimestamp(player_data);
|
||||
this.#signature_decipher_sc = this.#extractSigDecipherSc(player_data);
|
||||
this.#ntoken_decipher_sc = this.#extractNTokenSc(player_data);
|
||||
@@ -52,32 +57,64 @@ class Player {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current player's url.
|
||||
* @readonly
|
||||
* @returns {string}
|
||||
*/
|
||||
get url() {
|
||||
return this.#player_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the signature timestamp.
|
||||
* @readonly
|
||||
* @returns {string}
|
||||
*/
|
||||
get sts() {
|
||||
return this.#signature_timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the n-token decipher algorithm.
|
||||
* @readonly
|
||||
* @returns {string}
|
||||
*/
|
||||
get ntoken_decipher() {
|
||||
return this.#ntoken_decipher_sc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the signature decipher algorithm.
|
||||
* @readonly
|
||||
* @returns {string}
|
||||
*/
|
||||
get signature_decipher() {
|
||||
return this.#signature_decipher_sc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the signature timestamp from the player source code.
|
||||
* @returns {number}
|
||||
*/
|
||||
#extractSigTimestamp(data) {
|
||||
return parseInt(Utils.getStringBetweenStrings(data, 'signatureTimestamp:', ','));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the signature decipher algorithm.
|
||||
* @returns {string}
|
||||
*/
|
||||
#extractSigDecipherSc(data) {
|
||||
const sig_alg_sc = Utils.getStringBetweenStrings(data, 'this.audioTracks};var', '};');
|
||||
const sig_data = Utils.getStringBetweenStrings(data, 'function(a){a=a.split("")', 'return a.join("")}');
|
||||
return sig_alg_sc + sig_data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extracts the n-token decipher algorithm.
|
||||
* @returns {string}
|
||||
*/
|
||||
#extractNTokenSc(data) {
|
||||
return `var b=a.split("")${Utils.getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}')}} return b.join("");`;
|
||||
}
|
||||
|
||||
115
lib/core/PlaylistManager.js
Normal file
115
lib/core/PlaylistManager.js
Normal file
@@ -0,0 +1,115 @@
|
||||
'use strict';
|
||||
|
||||
const Utils = require('../utils/Utils');
|
||||
|
||||
/** @namespace */
|
||||
class PlaylistManager {
|
||||
#actions;
|
||||
|
||||
/**
|
||||
* @param {Actions} actions
|
||||
* @constructor
|
||||
*/
|
||||
constructor (actions) {
|
||||
this.#actions = actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a playlist.
|
||||
*
|
||||
* @param {string} title
|
||||
* @param {Array.<string>} video_ids
|
||||
*
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; playlist_id: string; data: object; }>}
|
||||
*/
|
||||
async create(title, video_ids) {
|
||||
Utils.throwIfMissing({ title, video_ids });
|
||||
|
||||
const response = await this.#actions.playlist('playlist/create', { title, ids: video_ids });
|
||||
|
||||
return {
|
||||
success: response.success,
|
||||
status_code: response.status_code,
|
||||
playlist_id: response.data.playlistId,
|
||||
data: response.data
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a given playlist.
|
||||
* @param {string} playlist_id
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; playlist_id: string; data: object; }>}
|
||||
*/
|
||||
async delete(playlist_id) {
|
||||
Utils.throwIfMissing({ playlist_id });
|
||||
|
||||
const response = await this.#actions.playlist('playlist/delete', { playlist_id });
|
||||
|
||||
return {
|
||||
success: response.success,
|
||||
status_code: response.status_code,
|
||||
playlist_id,
|
||||
data: response.data
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds videos to a given playlist.
|
||||
*
|
||||
* @param {string} playlist_id
|
||||
* @param {Array.<string>} video_ids
|
||||
*
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; playlist_id: string; data: object; }>}
|
||||
*/
|
||||
async addVideos(playlist_id, video_ids) {
|
||||
Utils.throwIfMissing({ playlist_id, video_ids });
|
||||
|
||||
const response = await this.#actions.playlist('browse/edit_playlist', {
|
||||
ids: video_ids,
|
||||
action: 'ACTION_ADD_VIDEO',
|
||||
playlist_id
|
||||
});
|
||||
|
||||
return {
|
||||
success: response.success,
|
||||
status_code: response.status_code,
|
||||
playlist_id,
|
||||
data: response.data
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes videos from a given playlist.
|
||||
*
|
||||
* @param {string} playlist_id
|
||||
* @param {Array.<string>} video_ids
|
||||
*
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; playlist_id: string; data: object; }>}
|
||||
*/
|
||||
async removeVideos(playlist_id, video_ids) {
|
||||
Utils.throwIfMissing({ playlist_id, video_ids });
|
||||
|
||||
const plinfo = await this.#actions.browse(`VL${playlist_id}`);
|
||||
|
||||
const list = Utils.findNode(plinfo.data, 'contents', 'contents', 13, false);
|
||||
if (!list.isEditable) throw new Utils.InnertubeError('This playlist cannot be edited.', playlist_id);
|
||||
|
||||
const videos = list.contents.filter((item) => video_ids.includes(item.playlistVideoRenderer.videoId));
|
||||
const set_video_ids = videos.map((video) => video.playlistVideoRenderer.setVideoId);
|
||||
|
||||
const response = await this.#actions.playlist('browse/edit_playlist', {
|
||||
ids: set_video_ids,
|
||||
action: 'ACTION_REMOVE_VIDEO',
|
||||
playlist_id
|
||||
});
|
||||
|
||||
return {
|
||||
success: response.success,
|
||||
status_code: response.status_code,
|
||||
playlist_id,
|
||||
data: response.data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PlaylistManager;
|
||||
@@ -7,6 +7,7 @@ const Utils = require('../utils/Utils');
|
||||
const Constants = require('../utils/Constants');
|
||||
const UserAgent = require('user-agents');
|
||||
|
||||
/** @namespace */
|
||||
class SessionBuilder {
|
||||
#config;
|
||||
|
||||
@@ -18,6 +19,10 @@ class SessionBuilder {
|
||||
#context;
|
||||
#player;
|
||||
|
||||
/**
|
||||
* @param {string} config
|
||||
* @constructor
|
||||
*/
|
||||
constructor(config) {
|
||||
this.#config = config;
|
||||
}
|
||||
@@ -42,6 +47,10 @@ class SessionBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a valid context object.
|
||||
* @returns
|
||||
*/
|
||||
#buildContext() {
|
||||
const user_agent = new UserAgent({ deviceCategory: 'desktop' });
|
||||
|
||||
@@ -70,6 +79,11 @@ class SessionBuilder {
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves initial configuration such as keys,
|
||||
* client data, etc.
|
||||
* @returns Promise.<object>
|
||||
*/
|
||||
async #getYtConfig() {
|
||||
const response = await Axios.get(`${Constants.URLS.YT_BASE}/sw.js_data`).catch((err) => err);
|
||||
|
||||
@@ -82,6 +96,10 @@ class SessionBuilder {
|
||||
return JSON.parse(response.data.replace(')]}\'', ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrives the YouTube player id.
|
||||
* @returns {Promise.<string>
|
||||
*/
|
||||
async #getPlayerId() {
|
||||
const response = await Axios.get(`${Constants.URLS.YT_BASE}/iframe_api`).catch((err) => err);
|
||||
|
||||
@@ -94,26 +112,32 @@ class SessionBuilder {
|
||||
return Utils.getStringBetweenStrings(response.data, 'player\\/', '\\/');
|
||||
}
|
||||
|
||||
/** @readonly */
|
||||
get key() {
|
||||
return this.#key;
|
||||
}
|
||||
|
||||
/** @readonly */
|
||||
get context() {
|
||||
return this.#context;
|
||||
}
|
||||
|
||||
/** @readonly */
|
||||
get api_version() {
|
||||
return this.#api_version;
|
||||
}
|
||||
|
||||
/** @readonly */
|
||||
get client_version() {
|
||||
return this.#client_version;
|
||||
}
|
||||
|
||||
/** @readonly */
|
||||
get client_name() {
|
||||
return this.#client_name;
|
||||
}
|
||||
|
||||
/** @readonly */
|
||||
get player() {
|
||||
return this.#player;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user