From a85e9ef667bb7af8fa8e0d99edd034e56459554f Mon Sep 17 00:00:00 2001 From: LuanRT Date: Fri, 27 May 2022 08:17:16 -0300 Subject: [PATCH] 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. --- lib/Innertube.js | 414 +++--------------- lib/core/AccountManager.js | 251 +++++++++++ lib/core/InteractionManager.js | 134 ++++++ lib/core/OAuth.js | 25 +- lib/core/Player.js | 45 +- lib/core/PlaylistManager.js | 115 +++++ lib/core/SessionBuilder.js | 24 + lib/parser/index.js | 2 +- lib/utils/Constants.js | 12 +- lib/utils/Request.js | 14 +- lib/utils/Utils.js | 1 + test/main.test.js | 4 +- typings/lib/Innertube.d.ts | 412 ++++------------- typings/lib/core/AccountManager.d.ts | 192 ++++++++ typings/lib/core/Actions.d.ts | 93 +++- typings/lib/core/InteractionManager.d.ts | 107 +++++ typings/lib/core/OAuth.d.ts | 21 +- typings/lib/core/Player.d.ts | 36 +- typings/lib/core/PlaylistManager.d.ts | 63 +++ typings/lib/core/SessionBuilder.d.ts | 25 +- .../youtube/search/SearchSuggestionItem.d.ts | 5 +- .../search/MusicSearchSuggestionItem.d.ts | 8 +- typings/lib/utils/Constants.d.ts | 16 +- typings/lib/utils/Request.d.ts | 9 +- typings/lib/utils/Utils.d.ts | 1 + 25 files changed, 1275 insertions(+), 754 deletions(-) create mode 100644 lib/core/AccountManager.js create mode 100644 lib/core/InteractionManager.js create mode 100644 lib/core/PlaylistManager.js create mode 100644 typings/lib/core/AccountManager.d.ts create mode 100644 typings/lib/core/InteractionManager.d.ts create mode 100644 typings/lib/core/PlaylistManager.d.ts diff --git a/lib/Innertube.js b/lib/Innertube.js index 804cbf3f..ec4a8124 100644 --- a/lib/Innertube.js +++ b/lib/Innertube.js @@ -10,6 +10,9 @@ const OAuth = require('./core/OAuth'); const Actions = require('./core/Actions'); const Livechat = require('./core/Livechat'); const SessionBuilder = require('./core/SessionBuilder'); +const AccountManager = require('./core/AccountManager'); +const PlaylistManager = require('./core/PlaylistManager'); +const InteractionManager = require('./core/InteractionManager'); const Utils = require('./utils/Utils'); const Request = require('./utils/Request'); @@ -19,11 +22,15 @@ const Proto = require('./proto'); const NToken = require('./deciphers/NToken'); const Signature = require('./deciphers/Signature'); +/** + * Innertube instance. + * @namespace + */ class Innertube { - #oauth; #player; /** + * @example * ```js * const Innertube = require('youtubei.js'); * const youtube = await new Innertube(); @@ -61,7 +68,7 @@ class Innertube { * @type {EventEmitter} */ this.ev = new EventEmitter(); - this.#oauth = new OAuth(this.ev); + this.oauth = new OAuth(this.ev); if (this.config.cookie) { this.auth_apisid = Utils.getStringBetweenStrings(this.config.cookie, 'PAPISID=', ';'); @@ -71,319 +78,29 @@ class Innertube { this.request = new Request(this); this.actions = new Actions(this); - this.#initMethods(); + this.account = new AccountManager(this.actions); + this.playlist = new PlaylistManager(this.actions); + this.interact = new InteractionManager(this.actions); return this; } - - #initMethods() { - this.account = { - info: () => this.getAccountInfo(), - getTimeWatched: () => { /* TODO: Implement this */ }, - settings: { - notifications: { - /** - * Notify about activity from the channels you're subscribed to. - * - * @param {boolean} new_value - ON | OFF - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - setSubscriptions: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SUBSCRIPTIONS, 'SPaccount_notifications', new_value), - - /** - * Recommended content notifications. - * - * @param {boolean} new_value - ON | OFF - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - setRecommendedVideos: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.RECOMMENDED_VIDEOS, 'SPaccount_notifications', new_value), - - /** - * Notify about activity on your channel. - * - * @param {boolean} new_value - ON | OFF - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - setChannelActivity: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.CHANNEL_ACTIVITY, 'SPaccount_notifications', new_value), - - /** - * Notify about replies to your comments. - * - * @param {boolean} new_value - ON | OFF - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - setCommentReplies: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.COMMENT_REPLIES, 'SPaccount_notifications', new_value), - - /** - * Notify when others mention your channel. - * - * @param {boolean} new_value - ON | OFF - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - setMentions: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.USER_MENTION, 'SPaccount_notifications', new_value), - - /** - * Notify when others share your content on their channels. - * - * @param {boolean} new_value - ON | OFF - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - setSharedContent: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SHARED_CONTENT, 'SPaccount_notifications', new_value) - }, - privacy: { - /** - * If set to true, your subscriptions won't be visible to others. - * - * @param {boolean} new_value - ON | OFF - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - setSubscriptionsPrivate: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SUBSCRIPTIONS_PRIVACY, 'SPaccount_privacy', new_value), - - /** - * If set to true, saved playlists won't appear on your channel. - * - * @param {boolean} new_value - ON | OFF - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - setSavedPlaylistsPrivate: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.PLAYLISTS_PRIVACY, 'SPaccount_privacy', new_value) - } - } - } - - this.interact = { - /** - * Likes a given video. - * - * @param {string} video_id - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - like: (video_id) => this.actions.engage('like/like', { video_id }), - - /** - * Diskes a given video. - * - * @param {string} video_id - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - dislike: (video_id) => this.actions.engage('like/dislike', { video_id }), - - /** - * Removes a like/dislike. - * - * @param {string} video_id - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - removeLike: (video_id) => this.actions.engage('like/removelike', { video_id }), - - /** - * Posts a comment on a given video. - * - * @param {string} video_id - * @param {string} text - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - comment: (video_id, text) => this.actions.engage('comment/create_comment', { video_id, text }), - - /** - * Translates a given text using YouTube's comment translate feature. - * - * @param {string} text - * @param {string} target_language - * @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; }>} - */ - translate: async (text, target_language, args = {}) => { - const response = await this.actions.engage('comment/perform_comment_action', { - text, - video_id: args.video_id, - comment_id: args.comment_id, - target_language: target_language, - comment_action: 'translate' - }); - - 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 - } - }, - - /** - * Subscribes to a given channel. - * - * @param {string} channel_id - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - subscribe: (channel_id) => this.actions.engage('subscription/subscribe', { channel_id }), - - /** - * Unsubscribes from a given channel. - * - * @param {string} channel_id - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - unsubscribe: (channel_id) => this.actions.engage('subscription/unsubscribe', { channel_id }), - - /** - * 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; }>} - */ - setNotificationPreferences: (channel_id, type) => this.actions.notifications('modify_channel_preference', { channel_id, pref: type || 'NONE' }), - }; - - this.playlist = { - /** - * Creates a playlist. - * - * @param {string} title - * @param {Array.} video_ids - * - * @returns {Promise.<{ success: boolean; status_code: number; playlist_id: string; data: object; }>} - */ - create: async (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; }>} - */ - delete: async (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 an array of videos to a given playlist. - * - * @param {string} playlist_id - * @param {Array.} video_ids - * @returns {Promise.<{ success: boolean; status_code: number; playlist_id: string; data: object; }>} - */ - addVideos: async (playlist_id, video_ids) => { - Utils.throwIfMissing({ playlist_id, video_ids }); - const response = await this.actions.playlist('browse/edit_playlist', { - action: 'ACTION_ADD_VIDEO', - playlist_id, - ids: video_ids - }); - - 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.} video_ids - * - * @returns {Promise.<{ success: boolean; status_code: number; playlist_id: string; data: object; }>} - */ - removeVideos: async (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', { - action: 'ACTION_REMOVE_VIDEO', - playlist_id, - ids: set_video_ids - }); - - return { - success: response.success, - status_code: response.status_code, - playlist_id, - data: response.data - } - } - }; - } /** - * Internal method to perform changes on an account's settings. - * - * @param {string} setting_id - * @param {string} type - * @param {string} new_value - * @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; - } - - /** - * Signs-in to a google account. + * Signs in to a google account. * * @param {object} auth_info * @param {string} auth_info.access_token - Token used to sign in. * @param {string} auth_info.refresh_token - Token used to get a new access token. - * @param {Date} auth_info.expires - Access token's expiration date, which is usually 24hrs-ish + * @param {Date} auth_info.expires - Access token's expiration date, which is usually 24hrs-ish. + * * @returns {Promise.} */ signIn(auth_info = {}) { return new Promise(async (resolve) => { - this.#oauth.init(auth_info); + this.oauth.init(auth_info); - if (this.#oauth.isValidAuthInfo()) { - await this.#oauth.checkTokenValidity(); + if (this.oauth.isValidAuthInfo()) { + await this.oauth.checkTokenValidity(); this.#updateCredentials(); return resolve(); } @@ -398,8 +115,8 @@ class Innertube { } #updateCredentials() { - this.access_token = this.#oauth.getAccessToken(); - this.refresh_token = this.#oauth.getRefreshToken(); + this.access_token = this.oauth.getAccessToken(); + this.refresh_token = this.oauth.getRefreshToken(); this.logged_in = true; } @@ -409,38 +126,13 @@ class Innertube { */ async signOut() { if (!this.logged_in) throw new Utils.InnertubeError('You are not signed in'); - const response = await this.#oauth.revokeAccessToken(); + const response = await this.oauth.revokeAccessToken(); response.success && (this.logged_in = false); return response; } - - /** - * Retrieves account details. - * @returns {Promise.<{ name: string; photo: Array; country: string; language: string; }>} - */ - async getAccountInfo() { - const response = await this.actions.account('account/account_menu'); - const menu = Utils.findNode(response, 'actions', 'multiPageMenuRenderer', 6, false); - - return { - name: menu.header.activeAccountHeaderRenderer.accountName.simpleText, - photo: menu.header.activeAccountHeaderRenderer.accountPhoto.thumbnails, - country: menu.sections[1].multiPageMenuSectionRenderer.items[2].compactLinkRenderer.subtitle.simpleText, - language: menu.sections[1].multiPageMenuSectionRenderer.items[1].compactLinkRenderer.subtitle.simpleText - } - } - - - // TEMPORARY: This is only here for testing purposes - // eslint-disable-next-line no-unused-private-class-members - async #getBasicChannelAnalytics() { - const params = Proto.encodeChannelAnalyticsParams('UCM9VCokI0KanBqPwq6QBv3g'); - const action = await this.actions.browse('FEanalytics_screen', { params, client: 'ANDROID' }); - return action; - } /** - * Searches on YouTube. + * Searches a given query. * * @param {string} query - search query. * @param {object} [options] - search options. @@ -451,8 +143,8 @@ class Innertube { * @param {string} [options.filters.duration] - filter videos by duration, can be: any | short | medium | long * @param {string} [options.filters.sort_by] - filter video results by order, can be: relevance | rating | upload_date | view_count * - * @returns {Promise.<{ query: string; corrected_query: string; estimated_results: number; videos: [] } | - * { results: { songs: []; videos: []; albums: []; community_playlists: [] } }>} + * @returns {Promise.<{ query: string; corrected_query: string; estimated_results: number; videos: object[] } | + * { results: { songs: object[]; videos: object[]; albums: object[]; community_playlists: object[] } }>} */ async search(query, options = { client: 'YOUTUBE' }) { Utils.throwIfMissing({ query }); @@ -469,22 +161,21 @@ class Innertube { } /** - * Retrieves search suggestions. + * Retrieves search suggestions for a given query. * - * @param {string} input - the search query. + * @param {string} query - the search query. * @param {object} [options] - search options. * @param {string} [options.client='YOUTUBE'] - client used to retrieve search suggestions, can be: `YOUTUBE` or `YTMUSIC`. * - * @returns {Promise.<[{ text: string; bold_text: string }]>} + * @returns {Promise.<{ query: string; results: string[] }>} */ - async getSearchSuggestions(input, options = { client: 'YOUTUBE' }) { - Utils.throwIfMissing({ input }); + async getSearchSuggestions(query, options = { client: 'YOUTUBE' }) { + Utils.throwIfMissing({ query }); - const response = await this.actions.getSearchSuggestions(options.client, input); + const response = await this.actions.getSearchSuggestions(options.client, query); if (options.client === 'YTMUSIC' && !response.data.contents) return []; const suggestions = new Parser(this, response.data, { - input, client: options.client, data_type: 'SEARCH_SUGGESTIONS' }).parse(); @@ -494,8 +185,7 @@ class Innertube { /** * Retrieves video info. - * - * @param {string} video_id - video id + * @param {string} video_id - the video id. * @return {Promise.<{ title: string; description: string; thumbnail: []; metadata: object }>} */ async getDetails(video_id) { @@ -528,11 +218,11 @@ class Innertube { } /** - * Retrieves comments for a video. + * Retrieves comments for a given video. * - * @param {string} video_id - video id + * @param {string} video_id - the video id. * @param {string} [sort_by] - can be: `TOP_COMMENTS` or `NEWEST_FIRST`. - * @return {Promise.<{ page_count: number; comment_count: number; items: []; }>} + * @return {Promise.<{ page_count: number; comment_count: number; items: object[]; }>} */ async getComments(video_id, sort_by) { Utils.throwIfMissing({ video_id }); @@ -553,7 +243,6 @@ class Innertube { /** * Retrieves contents for a given channel. (WIP) - * * @param {string} id - channel id * @return {Promise.<{ title: string; description: string; metadata: object; content: object }>} */ @@ -572,7 +261,7 @@ class Innertube { /** * Retrieves watch history. - * @returns {Promise.<{ items: [{ date: string; videos: [] }] }>} + * @returns {Promise.<{ items: { date: string; videos: object[] }[] }>} */ async getHistory() { const response = await this.actions.browse('FEhistory'); @@ -586,8 +275,8 @@ class Innertube { } /** - * Retrieves YouTube's home feed (aka recommendations). - * @returns {Promise.<{ videos: [{ id: string; title: string; description: string; channel: string; metadata: object }] }>} + * Retrieves home feed (aka recommendations). + * @returns {Promise.<{ videos: { id: string; title: string; description: string; channel: string; metadata: object }[] }>} */ async getHomeFeed() { const response = await this.actions.browse('FEwhat_to_watch'); @@ -602,10 +291,9 @@ class Innertube { /** * Retrieves trending content. - * - * @returns {Promise.<{ now: { content: [{ title: string; videos: []; }] }; - * music: { getVideos: Promise.; }; gaming: { getVideos: Promise.; }; - * movies: { getVideos: Promise.; } }>} + * @returns {Promise.<{ now: { content: { title: string; videos: object[]; }[] }; + * music: { getVideos: Promise.>; }; gaming: { getVideos: Promise.>; }; + * movies: { getVideos: Promise.>; } }>} */ async getTrending() { const response = await this.actions.browse('FEtrending'); @@ -619,6 +307,7 @@ class Innertube { } /** + * @todo finish this * WIP */ async getLibrary() { @@ -634,7 +323,7 @@ class Innertube { /** * Retrieves subscriptions feed. - * @returns {Promise.<{ items: [{ date: string; videos: [] }] }>} + * @returns {Promise.<{ items: { date: string; videos: object[] }[] }>} */ async getSubscriptionsFeed() { const response = await this.actions.browse('FEsubscriptions'); @@ -648,8 +337,8 @@ class Innertube { } /** - * Retrieves your notifications. - * @returns {Promise.<{ items: [{ title: string; sent_time: string; channel_name: string; channel_thumbnail: {}; video_thumbnail: {}; video_url: string; read: boolean; notification_id: string }] }>} + * Retrieves notifications. + * @returns {Promise.<{ items: { title: string; sent_time: string; channel_name: string; channel_thumbnail: object; video_thumbnail: object; video_url: string; read: boolean; notification_id: string }[] }>} */ async getNotifications() { const response = await this.actions.notifications('get_notification_menu'); @@ -691,11 +380,12 @@ class Innertube { } /** - * Retrieves a given playlist. + * Retrieves the contents of a given playlist. * - * @param {string} playlist_id - playlist id. - * @param {object} options - { client: YOUTUBE | YTMUSIC } + * @param {string} playlist_id - the id of the playlist. + * @param {object} options - `YOUTUBE` | `YTMUSIC` * @param {string} options.client - client used to parse the playlist, can be: `YTMUSIC` | `YOUTUBE` + * * @returns {Promise.< * { title: string; description: string; total_items: string; last_updated: string; views: string; items: [] } | * { title: string; description: string; total_items: number; duration: string; year: string; items: [] }>} @@ -717,6 +407,7 @@ class Innertube { * * @param {object} options * @param {object} video_data + * * @returns {object.<{ selected_format: {}; formats: [] }>} */ #chooseFormat(options, video_data) { @@ -784,7 +475,7 @@ class Innertube { * @param {string} options.type - download type, can be: video, audio or videoandaudio * @param {string} options.format - file format * - * @returns {Promise.<{ selected_format: {}; formats: [] }>} + * @returns {Promise.<{ selected_format: object; formats: object[] }>} */ async getStreamingData(video_id, options = {}) { Utils.throwIfMissing({ video_id }); @@ -810,6 +501,9 @@ class Innertube { * @param {string} [options.quality] - video quality; 360p, 720p, 1080p, etc... * @param {string} [options.type] - download type, can be: video, audio or videoandaudio * @param {string} [options.format] - file format + * @param {object} [options.range] - download range, indicates which bytes should be downloaded. + * @param {number} options.range.start - the beginning of the range. + * @param {number} options.range.end - the end of the range. * * @return {Stream.PassThrough} */ @@ -890,7 +584,7 @@ class Innertube { let must_end = false; stream.emit('start'); - + const downloadChunk = async () => { (chunk_end >= format.contentLength || options.range) && (must_end = true); options.range && (format.contentLength = options.range.end); diff --git a/lib/core/AccountManager.js b/lib/core/AccountManager.js new file mode 100644 index 00000000..17aba626 --- /dev/null +++ b/lib/core/AccountManager.js @@ -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; \ No newline at end of file diff --git a/lib/core/InteractionManager.js b/lib/core/InteractionManager.js new file mode 100644 index 00000000..9608c94e --- /dev/null +++ b/lib/core/InteractionManager.js @@ -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; \ No newline at end of file diff --git a/lib/core/OAuth.js b/lib/core/OAuth.js index cc01593e..29bc3815 100644 --- a/lib/core/OAuth.js +++ b/lib/core/OAuth.js @@ -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() { diff --git a/lib/core/Player.js b/lib/core/Player.js index dacd3571..45b3fc96 100644 --- a/lib/core/Player.js +++ b/lib/core/Player.js @@ -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("");`; } diff --git a/lib/core/PlaylistManager.js b/lib/core/PlaylistManager.js new file mode 100644 index 00000000..b368e5f4 --- /dev/null +++ b/lib/core/PlaylistManager.js @@ -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.} 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.} 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.} 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; \ No newline at end of file diff --git a/lib/core/SessionBuilder.js b/lib/core/SessionBuilder.js index 97926e37..39550872 100644 --- a/lib/core/SessionBuilder.js +++ b/lib/core/SessionBuilder.js @@ -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. + */ 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. + */ 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; } diff --git a/lib/parser/index.js b/lib/parser/index.js index b994b0fd..456cd1d0 100644 --- a/lib/parser/index.js +++ b/lib/parser/index.js @@ -119,7 +119,7 @@ class Parser { } #processSearchSuggestions() { - return YTDataItems.SearchSuggestionItem.parse(this.data[1], this.data[0]); + return YTDataItems.SearchSuggestionItem.parse(JSON.parse(this.data.replace(')]}\'', ''))); } #processMusicSearchSuggestions() { diff --git a/lib/utils/Constants.js b/lib/utils/Constants.js index cf854bc4..27379107 100644 --- a/lib/utils/Constants.js +++ b/lib/utils/Constants.js @@ -1,7 +1,5 @@ 'use strict'; -const Utils = require('./Utils'); - module.exports = { URLS: { YT_BASE: 'https://www.youtube.com', @@ -44,15 +42,15 @@ module.exports = { } }, STREAM_HEADERS: { - 'Accept': '*/*', - 'User-Agent': Utils.getRandomUserAgent('desktop').userAgent, - 'Connection': 'keep-alive', - 'Origin': 'https://www.youtube.com', - 'Referer': 'https://www.youtube.com', + 'accept': '*/*', + 'connection': 'keep-alive', + 'origin': 'https://www.youtube.com', + 'referer': 'https://www.youtube.com', 'DNT': '?1' }, INNERTUBE_HEADERS_BASE: { 'accept': '*/*', + 'accept-encoding': 'gzip, deflate', 'content-type': 'application/json', }, METADATA_KEYS: [ diff --git a/lib/utils/Request.js b/lib/utils/Request.js index fbb80b75..c929dd00 100644 --- a/lib/utils/Request.js +++ b/lib/utils/Request.js @@ -4,7 +4,12 @@ const Axios = require('axios'); const Utils = require('./Utils'); const Constants = require('./Constants'); +/** @namespace */ class Request { + /** + * @param {Innertube} session + * @constructor + */ constructor(session) { this.session = session; @@ -68,14 +73,17 @@ class Request { }, (error) => { throw new Utils.InnertubeError(error.message, error); }); - + + /** + * Standardizes the API response and catches all errors. + */ this.instance.interceptors.response.use((res) => { const response = { success: res.status === 200, status_code: res.status, data: res.data }; - + if (res.status !== 200) throw new Utils.InnertubeError(`Request to ${res.config.url} failed with status code ${res.status} ${res.statusText}`, response); @@ -89,7 +97,9 @@ class Request { } /** + * Adjusts the context according to the given client. * @todo refactor this? + * @returns */ #adjustContext(ctx, client) { switch (client) { diff --git a/lib/utils/Utils.js b/lib/utils/Utils.js index 78bfa15d..ccbbcdbb 100644 --- a/lib/utils/Utils.js +++ b/lib/utils/Utils.js @@ -4,6 +4,7 @@ const Crypto = require('crypto'); const UserAgent = require('user-agents'); const Flatten = require('flat'); +/** @namespace */ class InnertubeError extends Error { constructor (message, info) { super(message); diff --git a/test/main.test.js b/test/main.test.js index 328c27a6..72ae8b8f 100644 --- a/test/main.test.js +++ b/test/main.test.js @@ -24,12 +24,12 @@ describe('YouTube.js Tests', () => { it('Should retrieve YouTube search suggestions', async () => { const suggestions = await this.session.getSearchSuggestions(Constants.VIDEOS[0].QUERY, { client: 'YOUTUBE' }); - expect(suggestions.length).toBeLessThanOrEqual(10); + expect(suggestions.results.length).toBeLessThanOrEqual(10); }); it('Should retrieve YouTube Music search suggestions', async () => { const suggestions = await this.session.getSearchSuggestions(Constants.VIDEOS[1].QUERY, { client: 'YTMUSIC' }); - expect(suggestions.length).toBeLessThanOrEqual(10); + expect(suggestions.results.length).toBeLessThanOrEqual(10); }); }); diff --git a/typings/lib/Innertube.d.ts b/typings/lib/Innertube.d.ts index 4651d729..316848c7 100644 --- a/typings/lib/Innertube.d.ts +++ b/typings/lib/Innertube.d.ts @@ -1,6 +1,11 @@ export = Innertube; +/** + * Innertube instance. + * @namespace + */ declare class Innertube { /** + * @example * ```js * const Innertube = require('youtubei.js'); * const youtube = await new Innertube(); @@ -36,276 +41,21 @@ declare class Innertube { * @type {EventEmitter} */ ev: EventEmitter; + oauth: OAuth; auth_apisid: any; request: Request; actions: Actions; - account: { - info: () => Promise<{ - name: string; - photo: Array; - country: string; - language: string; - }>; - getTimeWatched: () => void; - settings: { - notifications: { - /** - * Notify about activity from the channels you're subscribed to. - * - * @param {boolean} new_value - ON | OFF - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - setSubscriptions: (new_value: boolean) => Promise<{ - success: boolean; - status_code: number; - data: object; - }>; - /** - * Recommended content notifications. - * - * @param {boolean} new_value - ON | OFF - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - setRecommendedVideos: (new_value: boolean) => Promise<{ - success: boolean; - status_code: number; - data: object; - }>; - /** - * Notify about activity on your channel. - * - * @param {boolean} new_value - ON | OFF - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - setChannelActivity: (new_value: boolean) => Promise<{ - success: boolean; - status_code: number; - data: object; - }>; - /** - * Notify about replies to your comments. - * - * @param {boolean} new_value - ON | OFF - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - setCommentReplies: (new_value: boolean) => Promise<{ - success: boolean; - status_code: number; - data: object; - }>; - /** - * Notify when others mention your channel. - * - * @param {boolean} new_value - ON | OFF - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - setMentions: (new_value: boolean) => Promise<{ - success: boolean; - status_code: number; - data: object; - }>; - /** - * Notify when others share your content on their channels. - * - * @param {boolean} new_value - ON | OFF - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - setSharedContent: (new_value: boolean) => Promise<{ - success: boolean; - status_code: number; - data: object; - }>; - }; - privacy: { - /** - * If set to true, your subscriptions won't be visible to others. - * - * @param {boolean} new_value - ON | OFF - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - setSubscriptionsPrivate: (new_value: boolean) => Promise<{ - success: boolean; - status_code: number; - data: object; - }>; - /** - * If set to true, saved playlists won't appear on your channel. - * - * @param {boolean} new_value - ON | OFF - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - setSavedPlaylistsPrivate: (new_value: boolean) => Promise<{ - success: boolean; - status_code: number; - data: object; - }>; - }; - }; - }; - interact: { - /** - * Likes a given video. - * - * @param {string} video_id - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - like: (video_id: string) => Promise<{ - success: boolean; - status_code: number; - data: object; - }>; - /** - * Diskes a given video. - * - * @param {string} video_id - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - dislike: (video_id: string) => Promise<{ - success: boolean; - status_code: number; - data: object; - }>; - /** - * Removes a like/dislike. - * - * @param {string} video_id - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - removeLike: (video_id: string) => Promise<{ - success: boolean; - status_code: number; - data: object; - }>; - /** - * Posts a comment on a given video. - * - * @param {string} video_id - * @param {string} text - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - comment: (video_id: string, text: string) => Promise<{ - success: boolean; - status_code: number; - data: object; - }>; - /** - * Translates a given text using YouTube's comment translate feature. - * - * @param {string} text - * @param {string} target_language - * @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; }>} - */ - translate: (text: string, target_language: string, args?: { - video_id?: string; - comment_id?: string; - }) => Promise<{ - success: boolean; - status_code: number; - translated_content: string; - data: object; - }>; - /** - * Subscribes to a given channel. - * - * @param {string} channel_id - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - subscribe: (channel_id: string) => Promise<{ - success: boolean; - status_code: number; - data: object; - }>; - /** - * Unsubscribes from a given channel. - * - * @param {string} channel_id - * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} - */ - unsubscribe: (channel_id: string) => Promise<{ - success: boolean; - status_code: number; - data: object; - }>; - /** - * 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; }>} - */ - setNotificationPreferences: (channel_id: string, type: string) => Promise<{ - success: boolean; - status_code: number; - data: object; - }>; - }; - playlist: { - /** - * Creates a playlist. - * - * @param {string} title - * @param {Array.} video_ids - * - * @returns {Promise.<{ success: boolean; status_code: number; playlist_id: string; data: object; }>} - */ - create: (title: string, video_ids: Array) => Promise<{ - success: boolean; - status_code: number; - playlist_id: string; - data: object; - }>; - /** - * Deletes a given playlist. - * - * @param {string} playlist_id - * @returns {Promise.<{ success: boolean; status_code: number; playlist_id: string; data: object; }>} - */ - delete: (playlist_id: string) => Promise<{ - success: boolean; - status_code: number; - playlist_id: string; - data: object; - }>; - /** - * Adds an array of videos to a given playlist. - * - * @param {string} playlist_id - * @param {Array.} video_ids - * @returns {Promise.<{ success: boolean; status_code: number; playlist_id: string; data: object; }>} - */ - addVideos: (playlist_id: string, video_ids: Array) => Promise<{ - success: boolean; - status_code: number; - playlist_id: string; - data: object; - }>; - /** - * Removes videos from a given playlist. - * - * @param {string} playlist_id - * @param {Array.} video_ids - * - * @returns {Promise.<{ success: boolean; status_code: number; playlist_id: string; data: object; }>} - */ - removeVideos: (playlist_id: string, video_ids: Array) => Promise<{ - success: boolean; - status_code: number; - playlist_id: string; - data: object; - }>; - }; + account: AccountManager; + playlist: PlaylistManager; + interact: InteractionManager; /** - * Signs-in to a google account. + * Signs in to a google account. * * @param {object} auth_info * @param {string} auth_info.access_token - Token used to sign in. * @param {string} auth_info.refresh_token - Token used to get a new access token. - * @param {Date} auth_info.expires - Access token's expiration date, which is usually 24hrs-ish + * @param {Date} auth_info.expires - Access token's expiration date, which is usually 24hrs-ish. + * * @returns {Promise.} */ signIn(auth_info?: { @@ -313,8 +63,8 @@ declare class Innertube { refresh_token: string; expires: Date; }): Promise; - access_token: any; - refresh_token: any; + access_token: string; + refresh_token: string; /** * Signs out of an account. * @returns {Promise.<{ success: boolean; status_code: number }>} @@ -324,17 +74,7 @@ declare class Innertube { status_code: number; }>; /** - * Retrieves account details. - * @returns {Promise.<{ name: string; photo: Array; country: string; language: string; }>} - */ - getAccountInfo(): Promise<{ - name: string; - photo: Array; - country: string; - language: string; - }>; - /** - * Searches on YouTube. + * Searches a given query. * * @param {string} query - search query. * @param {object} [options] - search options. @@ -345,8 +85,8 @@ declare class Innertube { * @param {string} [options.filters.duration] - filter videos by duration, can be: any | short | medium | long * @param {string} [options.filters.sort_by] - filter video results by order, can be: relevance | rating | upload_date | view_count * - * @returns {Promise.<{ query: string; corrected_query: string; estimated_results: number; videos: [] } | - * { results: { songs: []; videos: []; albums: []; community_playlists: [] } }>} + * @returns {Promise.<{ query: string; corrected_query: string; estimated_results: number; videos: object[] } | + * { results: { songs: object[]; videos: object[]; albums: object[]; community_playlists: object[] } }>} */ search(query: string, options?: { client?: string; @@ -360,34 +100,33 @@ declare class Innertube { query: string; corrected_query: string; estimated_results: number; - videos: []; + videos: object[]; } | { results: { - songs: []; - videos: []; - albums: []; - community_playlists: []; + songs: object[]; + videos: object[]; + albums: object[]; + community_playlists: object[]; }; }>; /** - * Retrieves search suggestions. + * Retrieves search suggestions for a given query. * - * @param {string} input - the search query. + * @param {string} query - the search query. * @param {object} [options] - search options. * @param {string} [options.client='YOUTUBE'] - client used to retrieve search suggestions, can be: `YOUTUBE` or `YTMUSIC`. * - * @returns {Promise.<[{ text: string; bold_text: string }]>} + * @returns {Promise.<{ query: string; results: string[] }>} */ - getSearchSuggestions(input: string, options?: { + getSearchSuggestions(query: string, options?: { client?: string; - }): Promise<[{ - text: string; - bold_text: string; - }]>; + }): Promise<{ + query: string; + results: string[]; + }>; /** * Retrieves video info. - * - * @param {string} video_id - video id + * @param {string} video_id - the video id. * @return {Promise.<{ title: string; description: string; thumbnail: []; metadata: object }>} */ getDetails(video_id: string): Promise<{ @@ -397,20 +136,19 @@ declare class Innertube { metadata: object; }>; /** - * Retrieves comments for a video. + * Retrieves comments for a given video. * - * @param {string} video_id - video id + * @param {string} video_id - the video id. * @param {string} [sort_by] - can be: `TOP_COMMENTS` or `NEWEST_FIRST`. - * @return {Promise.<{ page_count: number; comment_count: number; items: []; }>} + * @return {Promise.<{ page_count: number; comment_count: number; items: object[]; }>} */ getComments(video_id: string, sort_by?: string): Promise<{ page_count: number; comment_count: number; - items: []; + items: object[]; }>; /** * Retrieves contents for a given channel. (WIP) - * * @param {string} id - channel id * @return {Promise.<{ title: string; description: string; metadata: object; content: object }>} */ @@ -422,80 +160,80 @@ declare class Innertube { }>; /** * Retrieves watch history. - * @returns {Promise.<{ items: [{ date: string; videos: [] }] }>} + * @returns {Promise.<{ items: { date: string; videos: object[] }[] }>} */ getHistory(): Promise<{ - items: [{ + items: { date: string; - videos: []; - }]; + videos: object[]; + }[]; }>; /** - * Retrieves YouTube's home feed (aka recommendations). - * @returns {Promise.<{ videos: [{ id: string; title: string; description: string; channel: string; metadata: object }] }>} + * Retrieves home feed (aka recommendations). + * @returns {Promise.<{ videos: { id: string; title: string; description: string; channel: string; metadata: object }[] }>} */ getHomeFeed(): Promise<{ - videos: [{ + videos: { id: string; title: string; description: string; channel: string; metadata: object; - }]; + }[]; }>; /** * Retrieves trending content. - * - * @returns {Promise.<{ now: { content: [{ title: string; videos: []; }] }; - * music: { getVideos: Promise.; }; gaming: { getVideos: Promise.; }; - * movies: { getVideos: Promise.; } }>} + * @returns {Promise.<{ now: { content: { title: string; videos: object[]; }[] }; + * music: { getVideos: Promise.>; }; gaming: { getVideos: Promise.>; }; + * movies: { getVideos: Promise.>; } }>} */ getTrending(): Promise<{ now: { - content: [{ + content: { title: string; - videos: []; - }]; + videos: object[]; + }[]; }; music: { - getVideos: Promise; + getVideos: Promise>; }; gaming: { - getVideos: Promise; + getVideos: Promise>; }; movies: { - getVideos: Promise; + getVideos: Promise>; }; }>; /** + * @todo finish this * WIP */ getLibrary(): Promise; /** * Retrieves subscriptions feed. - * @returns {Promise.<{ items: [{ date: string; videos: [] }] }>} + * @returns {Promise.<{ items: { date: string; videos: object[] }[] }>} */ getSubscriptionsFeed(): Promise<{ - items: [{ + items: { date: string; - videos: []; - }]; + videos: object[]; + }[]; }>; /** - * Retrieves your notifications. - * @returns {Promise.<{ items: [{ title: string; sent_time: string; channel_name: string; channel_thumbnail: {}; video_thumbnail: {}; video_url: string; read: boolean; notification_id: string }] }>} + * Retrieves notifications. + * @returns {Promise.<{ items: { title: string; sent_time: string; channel_name: string; channel_thumbnail: object; video_thumbnail: object; video_url: string; read: boolean; notification_id: string }[] }>} */ getNotifications(): Promise<{ - items: [{ + items: { title: string; sent_time: string; channel_name: string; - channel_thumbnail: {}; - video_thumbnail: {}; + channel_thumbnail: object; + video_thumbnail: object; video_url: string; read: boolean; notification_id: string; - }]; + }[]; }>; /** * Retrieves unseen notifications count. @@ -510,11 +248,12 @@ declare class Innertube { */ getLyrics(video_id: string): Promise; /** - * Retrieves a given playlist. + * Retrieves the contents of a given playlist. * - * @param {string} playlist_id - playlist id. - * @param {object} options - { client: YOUTUBE | YTMUSIC } + * @param {string} playlist_id - the id of the playlist. + * @param {object} options - `YOUTUBE` | `YTMUSIC` * @param {string} options.client - client used to parse the playlist, can be: `YTMUSIC` | `YOUTUBE` + * * @returns {Promise.< * { title: string; description: string; total_items: string; last_updated: string; views: string; items: [] } | * { title: string; description: string; total_items: number; duration: string; year: string; items: [] }>} @@ -546,15 +285,15 @@ declare class Innertube { * @param {string} options.type - download type, can be: video, audio or videoandaudio * @param {string} options.format - file format * - * @returns {Promise.<{ selected_format: {}; formats: [] }>} + * @returns {Promise.<{ selected_format: object; formats: object[] }>} */ getStreamingData(video_id: string, options?: { quality: string; type: string; format: string; }): Promise<{ - selected_format: {}; - formats: []; + selected_format: object; + formats: object[]; }>; /** * Downloads a given video. If you only need the direct download link take a look at {@link getStreamingData}. @@ -564,6 +303,9 @@ declare class Innertube { * @param {string} [options.quality] - video quality; 360p, 720p, 1080p, etc... * @param {string} [options.type] - download type, can be: video, audio or videoandaudio * @param {string} [options.format] - file format + * @param {object} [options.range] - download range, indicates which bytes should be downloaded. + * @param {number} options.range.start - the beginning of the range. + * @param {number} options.range.end - the end of the range. * * @return {Stream.PassThrough} */ @@ -571,10 +313,18 @@ declare class Innertube { quality?: string; type?: string; format?: string; + range?: { + start: number; + end: number; + }; }): Stream.PassThrough; #private; } import EventEmitter = require("events"); +import OAuth = require("./core/OAuth"); import Request = require("./utils/Request"); import Actions = require("./core/Actions"); +import AccountManager = require("./core/AccountManager"); +import PlaylistManager = require("./core/PlaylistManager"); +import InteractionManager = require("./core/InteractionManager"); import Stream = require("stream"); diff --git a/typings/lib/core/AccountManager.d.ts b/typings/lib/core/AccountManager.d.ts new file mode 100644 index 00000000..5675c635 --- /dev/null +++ b/typings/lib/core/AccountManager.d.ts @@ -0,0 +1,192 @@ +export = AccountManager; +/** @namespace */ +declare class AccountManager { + /** + * @param {Actions} actions + * @constructor + */ + constructor(actions: Actions); + /** @namespace */ + channel: { + /** + * Edits channel name. + * + * @param {string} new_name + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + editName: (new_name: string) => Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Edits channel description. + * + * @param {string} new_description + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + editDescription: (new_description: string) => Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Retrieves basic channel analytics. + * @borrows AccountManager#getAnalytics as getBasicAnalytics + */ + getBasicAnalytics: () => 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; + }[]; + }>; + }; + /** @namespace */ + 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: boolean) => Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Recommended content notifications. + * + * @param {boolean} option - ON | OFF + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + setRecommendedVideos: (option: boolean) => Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Notify about activity on your channel. + * + * @param {boolean} option - ON | OFF + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + setChannelActivity: (option: boolean) => Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Notify about replies to your comments. + * + * @param {boolean} option - ON | OFF + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + setCommentReplies: (option: boolean) => Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Notify when others mention your channel. + * + * @param {boolean} option - ON | OFF + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + setMentions: (option: boolean) => Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * 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: boolean) => Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + }; + 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: boolean) => Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * 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: boolean) => Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + }; + }; + /** + * Retrieves channel info. + * @returns {Promise.<{ name: string; email: string; channel_id: string; subscriber_count: string; photo: object[]; }>} + */ + getInfo(): Promise<{ + name: string; + email: string; + channel_id: string; + subscriber_count: string; + photo: object[]; + }>; + /** + * Retrieves time watched statistics. + * @returns {Promise.<[{ title: string; time: string; }]>} + */ + getTimeWatched(): Promise<[{ + title: string; + time: string; + }]>; + /** + * 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 }[]; }>} + */ + getAnalytics(): 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; + }[]; + }>; + #private; +} diff --git a/typings/lib/core/Actions.d.ts b/typings/lib/core/Actions.d.ts index 05d2d669..1b227d5d 100644 --- a/typings/lib/core/Actions.d.ts +++ b/typings/lib/core/Actions.d.ts @@ -1,6 +1,11 @@ export = Actions; +/** namespace **/ declare class Actions { - constructor(session: any); + /** + * @param {Innertube} session + * @constructor + */ + constructor(session: Innertube); /** * Covers `/browse` endpoint, mostly used to access * YouTube's sections such as the home feed, etc @@ -53,21 +58,21 @@ declare class Actions { * * @param {string} action * @param {object} args - * @param {string} args.new_value - * @param {string} args.setting_item_id + * @param {string} [args.new_value] + * @param {string} [args.setting_item_id] * * @returns {Promise.<{ success: boolean; status_code: number; data: object }>} */ account(action: string, args?: { - new_value: string; - setting_item_id: string; + new_value?: string; + setting_item_id?: string; }): Promise<{ success: boolean; status_code: number; data: object; }>; /** - * Covers endpoint used for search. + * Endpoint used for search. * * @param {object} args * @param {string} [args.query] @@ -109,6 +114,24 @@ declare class Actions { status_code: number; data: object; }>; + /** + * Channel management endpoints. + * + * @param {string} action + * @param {object} args + * @param {string} [args.new_name] + * @param {string} [args.new_description] + * + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + channel(action: string, args?: { + new_name?: string; + new_description?: string; + }): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; /** * Covers endpoints used for playlist management. * @@ -175,6 +198,44 @@ declare class Actions { status_code: number; data: object; }>; + /** + * Endpoint used to retrieve video thumbnails. + * + * @param {object} args + * @param {string} args.video_id + * + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + thumbnails(args?: { + video_id: string; + }): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Place Autocomplete endpoint, found it in the APK but + * doesn't seem to be used anywhere on YouTube (maybe for ads?). + * + * Ex: + * ```js + * const places = await session.actions.geo('place_autocomplete', { input: 'San diego cafe' }); + * console.info(places.data); + * ``` + * + * @param {string} action + * @param {object} args + * @param {string} args.input + * + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + geo(action: string, args?: { + input: string; + }): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; /** * Covers endpoints used to report content. * @@ -212,13 +273,14 @@ declare class Actions { * @param {object} args * @param {string} [args.video_id] * @param {string} [args.ctoken] - * @param {string} [client] + * @param {string} [args.client] * * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} */ next(args?: { video_id?: string; ctoken?: string; + client?: string; }): Promise<{ success: boolean; status_code: number; @@ -245,7 +307,22 @@ declare class Actions { * * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} */ - getSearchSuggestions(client: string, input: string): Promise<{ + getSearchSuggestions(client: string, query: any): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Endpoint used to retrieve user mention suggestions. + * + * @param {object} args + * @param {string} args.input + * + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + getUserMentionSuggestions(args?: { + input: string; + }): Promise<{ success: boolean; status_code: number; data: object; diff --git a/typings/lib/core/InteractionManager.d.ts b/typings/lib/core/InteractionManager.d.ts new file mode 100644 index 00000000..6ac52468 --- /dev/null +++ b/typings/lib/core/InteractionManager.d.ts @@ -0,0 +1,107 @@ +export = InteractionManager; +/** @namespace */ +declare class InteractionManager { + /** + * @param {Actions} actions + * @constructor + */ + constructor(actions: Actions); + /** + * Likes a given video. + * @param {string} video_id + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + like(video_id: string): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Dislikes a given video. + * @param {string} video_id + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + dislike(video_id: string): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Removes a like/dislike. + * @param {string} video_id + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + removeLike(video_id: string): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Subscribes to a given channel. + * @param {string} channel_id + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + subscribe(channel_id: string): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Unsubscribes from a given channel. + * @param {string} channel_id + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + unsubscribe(channel_id: string): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Posts a comment on a given video. + * + * @param {string} video_id + * @param {string} text + * + * @returns {Promise.<{ success: boolean; status_code: number; data: object; }>} + */ + comment(video_id: string, text: string): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * 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; }>} + */ + translate(text: string, target_language: string, args?: { + video_id?: string; + comment_id?: string; + }): Promise<{ + success: boolean; + status_code: number; + translated_content: string; + data: object; + }>; + /** + * 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; }>} + */ + setNotificationPreferences(channel_id: string, type: string): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + #private; +} diff --git a/typings/lib/core/OAuth.d.ts b/typings/lib/core/OAuth.d.ts index 0f5bbd23..3bb60067 100644 --- a/typings/lib/core/OAuth.d.ts +++ b/typings/lib/core/OAuth.d.ts @@ -1,6 +1,11 @@ export = OAuth; +/** @namespace */ declare class OAuth { - constructor(ev: any); + /** + * @param {EventEmitter} ev + * @constructor + */ + constructor(ev: EventEmitter); /** * Starts the auth flow in case no valid credentials are available. * @returns {Promise.} @@ -18,10 +23,18 @@ declare class OAuth { * @returns {Promise.} */ revokeAccessToken(): Promise; - getAccessToken(): any; - getRefreshToken(): any; /** - * Checks if the auth info is valid. + * Returns the access token. + * @returns {string} + */ + getAccessToken(): string; + /** + * Returns the refresh token. + * @returns {string} + */ + getRefreshToken(): string; + /** + * Checks if the auth info format is valid. * @returns {boolean} true | false */ isValidAuthInfo(): boolean; diff --git a/typings/lib/core/Player.d.ts b/typings/lib/core/Player.d.ts index e54cd52b..8124f5c8 100644 --- a/typings/lib/core/Player.d.ts +++ b/typings/lib/core/Player.d.ts @@ -1,11 +1,37 @@ export = Player; +/** @namespace */ declare class Player { - constructor(id: any); + /** + * Represents the YouTube Web player script. + * @param {string} id - the id of the player. + * @constructor + */ + constructor(id: string); init(): Promise; - get url(): string; - get sts(): any; - get ntoken_decipher(): any; - get signature_decipher(): any; + /** + * Returns the current player's url. + * @readonly + * @returns {string} + */ + readonly get url(): string; + /** + * Returns the signature timestamp. + * @readonly + * @returns {string} + */ + readonly get sts(): string; + /** + * Returns the n-token decipher algorithm. + * @readonly + * @returns {string} + */ + readonly get ntoken_decipher(): string; + /** + * Returns the signature decipher algorithm. + * @readonly + * @returns {string} + */ + readonly get signature_decipher(): string; isCached(): boolean; #private; } diff --git a/typings/lib/core/PlaylistManager.d.ts b/typings/lib/core/PlaylistManager.d.ts new file mode 100644 index 00000000..1e58e914 --- /dev/null +++ b/typings/lib/core/PlaylistManager.d.ts @@ -0,0 +1,63 @@ +export = PlaylistManager; +/** @namespace */ +declare class PlaylistManager { + /** + * @param {Actions} actions + * @constructor + */ + constructor(actions: Actions); + /** + * Creates a playlist. + * + * @param {string} title + * @param {Array.} video_ids + * + * @returns {Promise.<{ success: boolean; status_code: number; playlist_id: string; data: object; }>} + */ + create(title: string, video_ids: Array): Promise<{ + success: boolean; + status_code: number; + playlist_id: string; + data: object; + }>; + /** + * Deletes a given playlist. + * @param {string} playlist_id + * @returns {Promise.<{ success: boolean; status_code: number; playlist_id: string; data: object; }>} + */ + delete(playlist_id: string): Promise<{ + success: boolean; + status_code: number; + playlist_id: string; + data: object; + }>; + /** + * Adds videos to a given playlist. + * + * @param {string} playlist_id + * @param {Array.} video_ids + * + * @returns {Promise.<{ success: boolean; status_code: number; playlist_id: string; data: object; }>} + */ + addVideos(playlist_id: string, video_ids: Array): Promise<{ + success: boolean; + status_code: number; + playlist_id: string; + data: object; + }>; + /** + * Removes videos from a given playlist. + * + * @param {string} playlist_id + * @param {Array.} video_ids + * + * @returns {Promise.<{ success: boolean; status_code: number; playlist_id: string; data: object; }>} + */ + removeVideos(playlist_id: string, video_ids: Array): Promise<{ + success: boolean; + status_code: number; + playlist_id: string; + data: object; + }>; + #private; +} diff --git a/typings/lib/core/SessionBuilder.d.ts b/typings/lib/core/SessionBuilder.d.ts index 15edb46a..ec12f47f 100644 --- a/typings/lib/core/SessionBuilder.d.ts +++ b/typings/lib/core/SessionBuilder.d.ts @@ -1,12 +1,23 @@ export = SessionBuilder; +/** @namespace */ declare class SessionBuilder { - constructor(config: any); + /** + * @param {string} config + * @constructor + */ + constructor(config: string); build(): Promise; - get key(): any; - get context(): any; - get api_version(): any; - get client_version(): any; - get client_name(): any; - get player(): any; + /** @readonly */ + readonly get key(): any; + /** @readonly */ + readonly get context(): any; + /** @readonly */ + readonly get api_version(): any; + /** @readonly */ + readonly get client_version(): any; + /** @readonly */ + readonly get client_name(): any; + /** @readonly */ + readonly get player(): any; #private; } diff --git a/typings/lib/parser/youtube/search/SearchSuggestionItem.d.ts b/typings/lib/parser/youtube/search/SearchSuggestionItem.d.ts index d382ab08..6a6c7294 100644 --- a/typings/lib/parser/youtube/search/SearchSuggestionItem.d.ts +++ b/typings/lib/parser/youtube/search/SearchSuggestionItem.d.ts @@ -1,4 +1,7 @@ export = SearchSuggestionItem; declare class SearchSuggestionItem { - static parse(data: any, bold_text: any): any; + static parse(data: any): { + query: any; + results: any; + }; } diff --git a/typings/lib/parser/ytmusic/search/MusicSearchSuggestionItem.d.ts b/typings/lib/parser/ytmusic/search/MusicSearchSuggestionItem.d.ts index 2891d377..434aa0ec 100644 --- a/typings/lib/parser/ytmusic/search/MusicSearchSuggestionItem.d.ts +++ b/typings/lib/parser/ytmusic/search/MusicSearchSuggestionItem.d.ts @@ -1,8 +1,8 @@ export = MusicSearchSuggestionItem; declare class MusicSearchSuggestionItem { - static parse(data: any): any; - static parseItem(item: any): { - text: any; - bold_text: any; + static parse(data: any): { + query: any; + results: any; }; + static parseItem(item: any): any; } diff --git a/typings/lib/utils/Constants.d.ts b/typings/lib/utils/Constants.d.ts index b34a3fc2..d76014b6 100644 --- a/typings/lib/utils/Constants.d.ts +++ b/typings/lib/utils/Constants.d.ts @@ -41,16 +41,16 @@ export namespace CLIENTS { export { VERSION_1 as VERSION }; } } -export const STREAM_HEADERS: { - Accept: string; - 'User-Agent': any; - Connection: string; - Origin: string; - Referer: string; - DNT: string; -}; +export namespace STREAM_HEADERS { + const accept: string; + const connection: string; + const origin: string; + const referer: string; + const DNT: string; +} export const INNERTUBE_HEADERS_BASE: { accept: string; + 'accept-encoding': string; 'content-type': string; }; export const METADATA_KEYS: string[]; diff --git a/typings/lib/utils/Request.d.ts b/typings/lib/utils/Request.d.ts index 97b7f033..d848246f 100644 --- a/typings/lib/utils/Request.d.ts +++ b/typings/lib/utils/Request.d.ts @@ -1,7 +1,12 @@ export = Request; +/** @namespace */ declare class Request { - constructor(session: any); - session: any; + /** + * @param {Innertube} session + * @constructor + */ + constructor(session: Innertube); + session: Innertube; instance: any; #private; } diff --git a/typings/lib/utils/Utils.d.ts b/typings/lib/utils/Utils.d.ts index 441a5876..2fdb46c7 100644 --- a/typings/lib/utils/Utils.d.ts +++ b/typings/lib/utils/Utils.d.ts @@ -1,3 +1,4 @@ +/** @namespace */ export class InnertubeError extends Error { constructor(message: any, info: any); info: any;