mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-14 01:52:11 +00:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0565ec924 | ||
|
|
15437e3937 | ||
|
|
c7c0ac8b54 | ||
|
|
1e23cdb510 | ||
|
|
a85e9ef667 | ||
|
|
865b6870a1 | ||
|
|
7284425618 | ||
|
|
05f74fe004 | ||
|
|
864f10f2e9 | ||
|
|
369e1048d1 | ||
|
|
b1cf5d33b8 | ||
|
|
19008e126d | ||
|
|
c525163f28 | ||
|
|
155dc9bd15 | ||
|
|
5560ba3ce4 | ||
|
|
6aaf9c70b9 | ||
|
|
e0c7496e37 | ||
|
|
fa79e5cad2 | ||
|
|
98a2b49395 | ||
|
|
17978193d0 | ||
|
|
13f571a6dc | ||
|
|
9f3f8ad820 | ||
|
|
2ba7a5c64e | ||
|
|
d7d1c96d8c | ||
|
|
0219c075c7 | ||
|
|
759351c38e | ||
|
|
6312e97f95 | ||
|
|
c60babcf25 | ||
|
|
c48cfcd8a0 | ||
|
|
594202d61d | ||
|
|
7a5490452a | ||
|
|
b4bb44b797 | ||
|
|
43f3c3fbf8 | ||
|
|
b48ae0b8d3 | ||
|
|
8cf3e67f79 | ||
|
|
ffa243bc07 | ||
|
|
a08580eeee | ||
|
|
039ebb7c0c | ||
|
|
46a385aa06 | ||
|
|
f656ccd690 | ||
|
|
ddd276d99f | ||
|
|
5fbeaeabb6 | ||
|
|
18e62f6ff8 | ||
|
|
6235985871 | ||
|
|
4eef0ddab0 | ||
|
|
6127690b4c | ||
|
|
b6cfdb733c | ||
|
|
b565213f11 | ||
|
|
a5c9c9d863 | ||
|
|
cf95d82d3e | ||
|
|
00e0131672 | ||
|
|
2315306d9f | ||
|
|
1dfd4b6263 | ||
|
|
b0a861dec8 | ||
|
|
4943685e57 | ||
|
|
b773f5668c | ||
|
|
4fd7371cf3 | ||
|
|
16bb879689 | ||
|
|
a852cd22c8 | ||
|
|
90bb3e20c0 | ||
|
|
eab40c0034 | ||
|
|
19f7336a48 | ||
|
|
75895e5492 | ||
|
|
0cdfac1812 | ||
|
|
446966fb2d | ||
|
|
29897981f0 | ||
|
|
7e8a517de9 | ||
|
|
a8b9487b58 | ||
|
|
80a338e5ff | ||
|
|
e2ca022a47 | ||
|
|
2ebcd49f02 | ||
|
|
98a62c31da | ||
|
|
1bfe2676d8 | ||
|
|
4db0a0358f | ||
|
|
6bdccb89e5 | ||
|
|
bbfecdb015 | ||
|
|
f79d4b635d | ||
|
|
283c06e64f | ||
|
|
5c572dba66 | ||
|
|
aa943a46a8 | ||
|
|
d634892b01 | ||
|
|
2010714f50 | ||
|
|
c6c96fd223 |
7
.eslintignore
Normal file
7
.eslintignore
Normal file
@@ -0,0 +1,7 @@
|
||||
.git
|
||||
.github
|
||||
test/
|
||||
cache/
|
||||
lib/proto/messages.js
|
||||
coverage/
|
||||
node_modules/
|
||||
45
.eslintrc.yml
Normal file
45
.eslintrc.yml
Normal file
@@ -0,0 +1,45 @@
|
||||
env:
|
||||
commonjs: true
|
||||
es2021: true
|
||||
node: true
|
||||
extends: eslint:recommended
|
||||
parserOptions:
|
||||
ecmaVersion: latest
|
||||
rules:
|
||||
max-len:
|
||||
- error
|
||||
-
|
||||
code: 200
|
||||
ignoreComments: true
|
||||
ignoreTrailingComments: true
|
||||
ignoreStrings: true
|
||||
ignoreTemplateLiterals: true
|
||||
ignoreRegExpLiterals: true
|
||||
|
||||
quotes: [error, single]
|
||||
|
||||
no-template-curly-in-string: error
|
||||
no-unreachable-loop: error
|
||||
no-unused-private-class-members: error
|
||||
no-prototype-builtins: 'off'
|
||||
no-async-promise-executor: 'off'
|
||||
no-case-declarations: 'off'
|
||||
no-return-assign: 'off'
|
||||
no-floating-decimal: error
|
||||
no-implied-eval: error
|
||||
arrow-spacing: error
|
||||
no-invalid-this: error
|
||||
no-lone-blocks: error
|
||||
no-new-func: error
|
||||
no-new-wrappers: error
|
||||
no-new: error
|
||||
no-void: error
|
||||
no-octal-escape: error
|
||||
no-self-compare: error
|
||||
no-sequences: error
|
||||
no-throw-literal: error
|
||||
no-unmodified-loop-condition: error
|
||||
no-useless-call: error
|
||||
no-useless-concat: error
|
||||
no-useless-escape: error
|
||||
no-useless-return: error
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -9,4 +9,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
custom: [ 'https://www.buymeacoffee.com/luanrt' ]
|
||||
17
.github/workflows/lint.yml
vendored
Normal file
17
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Lint
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
eslint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
|
||||
- name: npm install and lint
|
||||
run: |
|
||||
npm install
|
||||
npm run lint
|
||||
5
.github/workflows/node.js.yml
vendored
5
.github/workflows/node.js.yml
vendored
@@ -1,6 +1,3 @@
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Build
|
||||
|
||||
on:
|
||||
@@ -26,4 +23,4 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
- run: npm run test
|
||||
4
.github/workflows/stale.yml
vendored
4
.github/workflows/stale.yml
vendored
@@ -15,5 +15,5 @@ jobs:
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.'
|
||||
stale-pr-message: 'This PR has been automatically marked as stale because it has not had recent activity. Remove the stale label or comment or this will be closed in 2 days'
|
||||
days-before-stale: 6
|
||||
days-before-close: 2
|
||||
days-before-stale: 30
|
||||
days-before-close: 4
|
||||
52
.gitignore
vendored
Normal file
52
.gitignore
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
# YouTube player cache directory
|
||||
cache/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
.npmignore
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
7
jest.config.js
Normal file
7
jest.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
roots: [ '<rootDir>/test' ],
|
||||
testMatch: [ '**/*.test.js' ],
|
||||
testTimeout: 10000
|
||||
};
|
||||
655
lib/Innertube.js
655
lib/Innertube.js
@@ -7,9 +7,12 @@ const CancelToken = Axios.CancelToken;
|
||||
const EventEmitter = require('events');
|
||||
|
||||
const OAuth = require('./core/OAuth');
|
||||
const Player = require('./core/Player');
|
||||
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,286 +22,89 @@ const Proto = require('./proto');
|
||||
const NToken = require('./deciphers/NToken');
|
||||
const Signature = require('./deciphers/Signature');
|
||||
|
||||
/**
|
||||
* Innertube instance.
|
||||
* @namespace
|
||||
*/
|
||||
class Innertube {
|
||||
#oauth;
|
||||
#player;
|
||||
#retry_count;
|
||||
|
||||
/**
|
||||
* @example
|
||||
* ```js
|
||||
* const Innertube = require('youtubei.js');
|
||||
* const youtube = await new Innertube();
|
||||
* ```
|
||||
*
|
||||
* @param {object} [config]
|
||||
* @param {string} [config.gl]
|
||||
* @param {string} [config.cookie]
|
||||
* @param {boolean} [config.debug]
|
||||
*
|
||||
* @returns {Innertube}
|
||||
* @constructor
|
||||
*/
|
||||
constructor(config) {
|
||||
this.config = config || {};
|
||||
this.#retry_count = 0;
|
||||
return this.#init();
|
||||
}
|
||||
|
||||
|
||||
async #init() {
|
||||
const response = await Axios.get(Constants.URLS.YT_BASE, Constants.DEFAULT_HEADERS(this.config)).catch((error) => error);
|
||||
if (response instanceof Error) throw new Utils.InnertubeError('Could not retrieve Innertube session', { message: response.message, status_code: response.status || 0 });
|
||||
|
||||
const data = JSON.parse(`{${Utils.getStringBetweenStrings(response.data, 'ytcfg.set({', '});') || ''}}`);
|
||||
if (data.INNERTUBE_CONTEXT) {
|
||||
this.key = data.INNERTUBE_API_KEY;
|
||||
this.version = data.INNERTUBE_API_VERSION;
|
||||
this.context = data.INNERTUBE_CONTEXT;
|
||||
|
||||
this.player_url = data.PLAYER_JS_URL;
|
||||
this.logged_in = data.LOGGED_IN;
|
||||
this.sts = data.STS;
|
||||
|
||||
this.context.client.hl = 'en';
|
||||
this.context.client.gl = this.config.gl || 'US';
|
||||
|
||||
/**
|
||||
* @event Innertube#auth - Fired when signing in to an account.
|
||||
* @event Innertube#update-credentials - Fired when the access token is no longer valid.
|
||||
* @type {EventEmitter}
|
||||
*/
|
||||
this.ev = new EventEmitter();
|
||||
this.#oauth = new OAuth(this.ev);
|
||||
|
||||
this.#player = new Player(this);
|
||||
await this.#player.init();
|
||||
|
||||
if (this.logged_in && this.config.cookie) {
|
||||
this.auth_apisid = Utils.getStringBetweenStrings(this.config.cookie, 'PAPISID=', ';');
|
||||
this.auth_apisid = Utils.generateSidAuth(this.auth_apisid);
|
||||
}
|
||||
|
||||
this.request = new Request(this);
|
||||
|
||||
this.#initMethods();
|
||||
} else {
|
||||
this.#retry_count += 1;
|
||||
if (this.#retry_count >= 10)
|
||||
throw new Utils.ParsingError('No InnerTubeContext shell provided in ytconfig.', {
|
||||
data_snippet: response.data.slice(0, 300),
|
||||
status_code: response.status || 0
|
||||
});
|
||||
return this.#init();
|
||||
const session = await new SessionBuilder(this.config).build();
|
||||
|
||||
this.key = session.key;
|
||||
this.version = session.api_version;
|
||||
this.context = session.context;
|
||||
|
||||
this.logged_in = false;
|
||||
this.player_url = session.player.url;
|
||||
this.sts = session.player.sts;
|
||||
|
||||
this.#player = session.player;
|
||||
|
||||
/**
|
||||
* @fires Innertube#auth - fired when signing in to an account.
|
||||
* @fires Innertube#update-credentials - fired when the access token is no longer valid.
|
||||
* @type {EventEmitter}
|
||||
*/
|
||||
this.ev = new EventEmitter();
|
||||
this.oauth = new OAuth(this.ev);
|
||||
|
||||
if (this.config.cookie) {
|
||||
this.auth_apisid = Utils.getStringBetweenStrings(this.config.cookie, 'PAPISID=', ';');
|
||||
this.auth_apisid = Utils.generateSidAuth(this.auth_apisid);
|
||||
}
|
||||
|
||||
this.request = new Request(this);
|
||||
this.actions = new Actions(this);
|
||||
|
||||
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(),
|
||||
settings: {
|
||||
notifications: {
|
||||
/**
|
||||
* Notify about activity from the channels you're subscribed to.
|
||||
*
|
||||
* @param {boolean} new_value
|
||||
* @returns {Promise.<{ success: boolean; status_code: string; }>}
|
||||
*/
|
||||
setSubscriptions: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SUBSCRIPTIONS, 'account_notifications', new_value),
|
||||
|
||||
/**
|
||||
* Recommended content notifications.
|
||||
*
|
||||
* @param {boolean} new_value
|
||||
* @returns {Promise.<{ success: boolean; status_code: string; }>}
|
||||
*/
|
||||
setRecommendedVideos: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.RECOMMENDED_VIDEOS, 'account_notifications', new_value),
|
||||
|
||||
/**
|
||||
* Notify about activity on your channel.
|
||||
*
|
||||
* @param {boolean} new_value
|
||||
* @returns {Promise.<{ success: boolean; status_code: string; }>}
|
||||
*/
|
||||
setChannelActivity: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.CHANNEL_ACTIVITY, 'account_notifications', new_value),
|
||||
|
||||
/**
|
||||
* Notify about replies to your comments.
|
||||
*
|
||||
* @param {boolean} new_value
|
||||
* @returns {Promise.<{ success: boolean; status_code: string; }>}
|
||||
*/
|
||||
setCommentReplies: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.COMMENT_REPLIES, 'account_notifications', new_value),
|
||||
|
||||
/**
|
||||
* Notify when others mention your channel.
|
||||
*
|
||||
* @param {boolean} new_value
|
||||
* @returns {Promise.<{ success: boolean; status_code: string; }>}
|
||||
*/
|
||||
setMentions: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.USER_MENTION, 'account_notifications', new_value),
|
||||
|
||||
/**
|
||||
* Notify when others share your content on their channels.
|
||||
*
|
||||
* @param {boolean} new_value
|
||||
* @returns {Promise.<{ success: boolean; status_code: string; }>}
|
||||
*/
|
||||
setSharedContent: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SHARED_CONTENT, 'account_notifications', new_value)
|
||||
},
|
||||
privacy: {
|
||||
/**
|
||||
* If set to true, your subscriptions won't be visible to others.
|
||||
*
|
||||
* @param {boolean} new_value
|
||||
* @returns {Promise.<{ success: boolean; status_code: string; }>}
|
||||
*/
|
||||
setSubscriptionsPrivate: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SUBSCRIPTIONS_PRIVACY, 'account_privacy', new_value),
|
||||
|
||||
/**
|
||||
* If set to true, saved playlists won't appear on your channel.
|
||||
*
|
||||
* @param {boolean} new_value
|
||||
* @returns {Promise.<{ success: boolean; status_code: string; }>}
|
||||
*/
|
||||
setSavedPlaylistsPrivate: (new_value) => this.#setSetting(Constants.ACCOUNT_SETTINGS.PLAYLISTS_PRIVACY, 'account_privacy', new_value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.interact = {
|
||||
/**
|
||||
* Likes a given video.
|
||||
*
|
||||
* @param {string} video_id
|
||||
* @returns {Promise.<{ success: boolean; status_code: string; }>}
|
||||
*/
|
||||
like: (video_id) => Actions.engage(this, 'like/like', { video_id }),
|
||||
|
||||
/**
|
||||
* Diskes a given video.
|
||||
*
|
||||
* @param {string} video_id
|
||||
* @returns {Promise.<{ success: boolean; status_code: string; }>}
|
||||
*/
|
||||
dislike: (video_id) => Actions.engage(this, 'like/dislike', { video_id }),
|
||||
|
||||
/**
|
||||
* Removes a like/dislike.
|
||||
*
|
||||
* @param {string} video_id
|
||||
* @returns {Promise.<{ success: boolean; status_code: string; }>}
|
||||
*/
|
||||
removeLike: (video_id) => Actions.engage(this, 'like/removelike', { video_id }),
|
||||
|
||||
/**
|
||||
* Posts a comment on a given video.
|
||||
*
|
||||
* @param {string} video_id
|
||||
* @param {string} text
|
||||
* @returns {Promise.<{ success: boolean; status_code: string; }>}
|
||||
*/
|
||||
comment: (video_id, text) => Actions.engage(this, 'comment/create_comment', { video_id, text }),
|
||||
|
||||
/**
|
||||
* Subscribes to a given channel.
|
||||
*
|
||||
* @param {string} channel_id
|
||||
* @returns {Promise.<{ success: boolean; status_code: string; }>}
|
||||
*/
|
||||
subscribe: (channel_id) => Actions.engage(this, 'subscription/subscribe', { channel_id }),
|
||||
|
||||
/**
|
||||
* Unsubscribes from a given channel.
|
||||
*
|
||||
* @param {string} channel_id
|
||||
* @returns {Promise.<{ success: boolean; status_code: string; }>}
|
||||
*/
|
||||
unsubscribe: (channel_id) => Actions.engage(this, '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: string; }>}
|
||||
*/
|
||||
setNotificationPreferences: (channel_id, type) => Actions.notifications(this, 'modify_channel_preference', { channel_id, pref: type || 'NONE' }),
|
||||
};
|
||||
|
||||
this.playlist = {
|
||||
/**
|
||||
* Creates a playlist.
|
||||
*
|
||||
* @param {string} title
|
||||
* @param {string} video_id - Note that a video must be supplied, empty playlists cannot be created.
|
||||
* @returns {Promise.<{ success: boolean; status_code: string; }>}
|
||||
*/
|
||||
create: (title, video_id) => Actions.engage(this, 'playlist/create', { title, video_id }),
|
||||
|
||||
/**
|
||||
* Deletes a given playlist.
|
||||
*
|
||||
* @param {string} playlist_id
|
||||
* @returns {Promise.<{ success: boolean; status_code: string; }>}
|
||||
*/
|
||||
delete: (playlist_id) => Actions.engage(this, 'playlist/delete', { playlist_id }),
|
||||
|
||||
/**
|
||||
* Adds an array of videos to a given playlist.
|
||||
*
|
||||
* @param {string} playlist_id
|
||||
* @param {Array.<string>} video_ids
|
||||
* @returns {Promise.<{ success: boolean; status_code: string; }>}
|
||||
*/
|
||||
addVideos: (playlist_id, video_ids) => Actions.engage(this, 'browse/edit_playlist', { action: 'ACTION_ADD_VIDEO', playlist_id, video_ids })
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: string; }>}
|
||||
*/
|
||||
async #setSetting(setting_id, type, new_value) {
|
||||
const response = await Actions.browse(this, type);
|
||||
if (!response.success) return response;
|
||||
|
||||
const contents = ({
|
||||
account_notifications: () => Utils.findNode(response.data, 'contents', 'Your preferences', 13, false).options,
|
||||
account_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 Actions.account(this, 'account/set_setting', { new_value: type == 'account_privacy' ? !new_value : new_value, setting_item_id });
|
||||
|
||||
return {
|
||||
success: set_setting.success,
|
||||
status_code: set_setting.status_code,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.<void>}
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
this.ev.on('auth', (data) => {
|
||||
if (data.status === 'SUCCESS') {
|
||||
this.#updateCredentials();
|
||||
@@ -307,60 +113,47 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Signs out of your account.
|
||||
* Signs out of an account.
|
||||
* @returns {Promise.<{ success: boolean; status_code: number }>}
|
||||
*/
|
||||
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<object>; country: string; language: string; }>}
|
||||
*/
|
||||
async getAccountInfo() {
|
||||
const response = await Actions.account(this, 'account/account_menu');
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not get account info', response);
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches on YouTube.
|
||||
* Searches a given query.
|
||||
*
|
||||
* @param {string} query - Search query.
|
||||
* @param {object} options - Search options.
|
||||
* @param {string} options.client - Client used to perform the search, can be: `YTMUSIC` or `YOUTUBE`.
|
||||
* @param {string} options.period - Filter videos uploaded within a period, can be: any | hour | day | week | month | year
|
||||
* @param {string} options.order - Filter results by order, can be: relevance | rating | age | views
|
||||
* @param {string} options.duration - Filter video results by duration, can be: any | short | long
|
||||
* @returns {Promise.<{ query: string; corrected_query: string; estimated_results: number; videos: [] } |
|
||||
* { results: { songs: []; videos: []; albums: []; community_playlists: [] } }>}
|
||||
* @param {string} query - search query.
|
||||
* @param {object} [options] - search options.
|
||||
* @param {string} [options.client] - client used to perform the search, can be: `YTMUSIC` or `YOUTUBE`.
|
||||
* @param {object} [options.filters] - search filters.
|
||||
* @param {string} [options.filters.upload_date] - filter videos by upload date, can be: any | last_hour | today | this_week | this_month | this_year
|
||||
* @param {string} [options.filters.type] - filter results by type, can be: any | video | channel | playlist | movie
|
||||
* @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: object[] } |
|
||||
* { results: { songs: object[]; videos: object[]; albums: object[]; community_playlists: object[] } }>}
|
||||
*/
|
||||
async search(query, options = { client: 'YOUTUBE', period: 'any', order: 'relevance', duration: 'any' }) {
|
||||
const response = await Actions.search(this, options.client, { query, options });
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not search on YouTube', response);
|
||||
async search(query, options = { client: 'YOUTUBE' }) {
|
||||
Utils.throwIfMissing({ query });
|
||||
|
||||
const response = await this.actions.search({ query, options, client: options.client });
|
||||
|
||||
const results = new Parser(this, response.data, {
|
||||
query, client: options.client,
|
||||
query,
|
||||
client: options.client,
|
||||
data_type: 'SEARCH'
|
||||
}).parse();
|
||||
|
||||
@@ -368,159 +161,175 @@ class Innertube {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves search suggestions.
|
||||
* Retrieves search suggestions for a given query.
|
||||
*
|
||||
* @param {string} input - 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 }]>}
|
||||
* @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.<{ query: string; results: string[] }>}
|
||||
*/
|
||||
async getSearchSuggestions(input, options = { client: 'YOUTUBE' }) {
|
||||
const response = await Actions.getSearchSuggestions(this, options.client, input);
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not get search suggestions', response);
|
||||
async getSearchSuggestions(query, options = { client: 'YOUTUBE' }) {
|
||||
Utils.throwIfMissing({ query });
|
||||
|
||||
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,
|
||||
client: options.client,
|
||||
data_type: 'SEARCH_SUGGESTIONS'
|
||||
}).parse();
|
||||
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (!video_id) throw new Utils.MissingParamError('Video id is missing');
|
||||
|
||||
const response = await Actions.getVideoInfo(this, { id: video_id });
|
||||
const continuation = await Actions.next(this, { video_id });
|
||||
continuation.success && (response.continuation = continuation.data);
|
||||
|
||||
Utils.throwIfMissing({ video_id });
|
||||
|
||||
const response = await this.actions.getVideoInfo(video_id);
|
||||
const continuation = await this.actions.next({ video_id });
|
||||
response.continuation = continuation.data;
|
||||
|
||||
const details = new Parser(this, response, {
|
||||
client: 'YOUTUBE',
|
||||
client: 'YOUTUBE',
|
||||
data_type: 'VIDEO_INFO'
|
||||
}).parse();
|
||||
|
||||
details.like = () => Actions.engage(this, 'like/like', { video_id });
|
||||
details.dislike = () => Actions.engage(this, 'like/dislike', { video_id });
|
||||
details.removeLike = () => Actions.engage(this, 'like/removelike', { video_id });
|
||||
details.subscribe = () => Actions.engage(this, 'subscription/subscribe', { channel_id: details.metadata.channel_id });
|
||||
details.unsubscribe = () => Actions.engage(this, 'subscription/unsubscribe', { channel_id: details.metadata.channel_id });
|
||||
details.comment = (text) => Actions.engage(this, 'comment/create_comment', { video_id, text });
|
||||
|
||||
const livechat_ctoken = continuation.data.contents?.twoColumnWatchNextResults
|
||||
?.conversationBar?.liveChatRenderer?.continuations?.find((continuation) => continuation.reloadContinuationData)
|
||||
.reloadContinuationData.continuation;
|
||||
|
||||
details.like = () => this.actions.engage('like/like', { video_id });
|
||||
details.dislike = () => this.actions.engage('like/dislike', { video_id });
|
||||
details.removeLike = () => this.actions.engage('like/removelike', { video_id });
|
||||
details.subscribe = () => this.actions.engage('subscription/subscribe', { channel_id: details.metadata.channel_id });
|
||||
details.unsubscribe = () => this.actions.engage('subscription/unsubscribe', { channel_id: details.metadata.channel_id });
|
||||
details.comment = (text) => this.actions.engage('comment/create_comment', { video_id, text });
|
||||
details.getComments = (sort_by) => this.getComments(video_id, sort_by);
|
||||
details.getLivechat = () => new Livechat(this, continuation.data.contents?.twoColumnWatchNextResults?.conversationBar?.liveChatRenderer?.continuations?.find((continuation) => continuation.reloadContinuationData).reloadContinuationData.continuation, details.metadata.channel_id, video_id);
|
||||
details.setNotificationPreferences = (type) => Actions.notifications(this, 'modify_channel_preference', { channel_id: details.metadata.channel_id, pref: type || 'NONE' });
|
||||
details.getLivechat = () => new Livechat(this, livechat_ctoken, details.metadata.channel_id, video_id);
|
||||
details.setNotificationPreferences = (type) => this.actions.notifications('modify_channel_preference', { channel_id: details.metadata.channel_id, pref: type || 'NONE' });
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves comments for a video.
|
||||
* Retrieves comments for a given video.
|
||||
*
|
||||
* @param {string} video_id - Video id
|
||||
* @param {string} [sort_by] - Can be: `TOP_COMMENTS` or `NEWEST_FIRST`.
|
||||
* @return {Promise.<{ page_count: number; comment_count: number; items: []; }>}
|
||||
* @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: object[]; }>}
|
||||
*/
|
||||
async getComments(video_id, sort_by) {
|
||||
Utils.throwIfMissing({ video_id });
|
||||
|
||||
const payload = Proto.encodeCommentsSectionParams(video_id, {
|
||||
sort_by: sort_by || 'TOP_COMMENTS'
|
||||
});
|
||||
|
||||
const response = await Actions.next(this, { continuation_token: payload });
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve comments', response);
|
||||
|
||||
const response = await this.actions.next({ ctoken: payload });
|
||||
const comments = new Parser(this, response.data, {
|
||||
video_id,
|
||||
client: 'YOUTUBE',
|
||||
client: 'YOUTUBE',
|
||||
data_type: 'COMMENTS'
|
||||
}).parse();
|
||||
|
||||
|
||||
return comments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves contents for a given channel. (WIP)
|
||||
*
|
||||
* @param {string} id - The id of the channel.
|
||||
* @param {string} id - channel id
|
||||
* @return {Promise.<{ title: string; description: string; metadata: object; content: object }>}
|
||||
*/
|
||||
async getChannel(id) {
|
||||
const response = await Actions.browse(this, 'channel', { browse_id: id });
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve channel info.', response);
|
||||
Utils.throwIfMissing({ id });
|
||||
|
||||
const response = await this.actions.browse(id);
|
||||
|
||||
const channel_info = new Parser(this, response.data, {
|
||||
client: 'YOUTUBE',
|
||||
client: 'YOUTUBE',
|
||||
data_type: 'CHANNEL'
|
||||
}).parse();
|
||||
|
||||
|
||||
return channel_info;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves your watch history.
|
||||
* @returns {Promise.<{ items: [{ date: string; videos: [] }] }>}
|
||||
* Retrieves watch history.
|
||||
* @returns {Promise.<{ items: { date: string; videos: object[] }[] }>}
|
||||
*/
|
||||
async getHistory() {
|
||||
const response = await Actions.browse(this, 'history');
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve watch history', response);
|
||||
|
||||
const response = await this.actions.browse('FEhistory');
|
||||
|
||||
const history = new Parser(this, response, {
|
||||
client: 'YOUTUBE',
|
||||
client: 'YOUTUBE',
|
||||
data_type: 'HISTORY'
|
||||
}).parse();
|
||||
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Actions.browse(this, 'home_feed');
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve home feed', response);
|
||||
|
||||
const response = await this.actions.browse('FEwhat_to_watch');
|
||||
|
||||
const homefeed = new Parser(this, response, {
|
||||
client: 'YOUTUBE',
|
||||
client: 'YOUTUBE',
|
||||
data_type: 'HOMEFEED'
|
||||
}).parse();
|
||||
|
||||
|
||||
return homefeed;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves trending content.
|
||||
* @returns {Promise.<{ now: { content: [{ title: string; videos: []; }] };
|
||||
* music: { getVideos: Promise.<Array>; }; gaming: { getVideos: Promise.<Array>; };
|
||||
* gaming: { getVideos: Promise.<Array>; }; }>}
|
||||
* @returns {Promise.<{ now: { content: { title: string; videos: object[]; }[] };
|
||||
* music: { getVideos: Promise.<Array.<object>>; }; gaming: { getVideos: Promise.<Array.<object>>; };
|
||||
* movies: { getVideos: Promise.<Array.<object>>; } }>}
|
||||
*/
|
||||
async getTrending() {
|
||||
const response = await Actions.browse(this, 'trending');
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve trending content', response);
|
||||
const response = await this.actions.browse('FEtrending');
|
||||
|
||||
const trending = new Parser(this, response, {
|
||||
client: 'YOUTUBE',
|
||||
data_type: 'TRENDING'
|
||||
}).parse();
|
||||
|
||||
|
||||
return trending;
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo finish this
|
||||
* WIP
|
||||
*/
|
||||
async getLibrary() {
|
||||
const response = await this.actions.browse('FElibrary');
|
||||
|
||||
const library = new Parser(this, response.data, {
|
||||
client: 'YOUTUBE',
|
||||
data_type: 'LIBRARY'
|
||||
}).parse();
|
||||
|
||||
return library;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves your subscriptions feed.
|
||||
* @returns {Promise.<{ items: [{ date: string; videos: [] }] }>}
|
||||
* Retrieves subscriptions feed.
|
||||
* @returns {Promise.<{ items: { date: string; videos: object[] }[] }>}
|
||||
*/
|
||||
async getSubscriptionsFeed() {
|
||||
const response = await Actions.browse(this, 'subscriptions_feed');
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve subscriptions feed', response);
|
||||
|
||||
const response = await this.actions.browse('FEsubscriptions');
|
||||
|
||||
const subsfeed = new Parser(this, response, {
|
||||
client: 'YOUTUBE',
|
||||
client: 'YOUTUBE',
|
||||
data_type: 'SUBSFEED'
|
||||
}).parse();
|
||||
|
||||
@@ -528,28 +337,26 @@ 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 Actions.notifications(this, 'get_notification_menu');
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not fetch notifications', response);
|
||||
const response = await this.actions.notifications('get_notification_menu');
|
||||
|
||||
const notifications = new Parser(this, response.data, {
|
||||
client: 'YOUTUBE',
|
||||
client: 'YOUTUBE',
|
||||
data_type: 'NOTIFICATIONS'
|
||||
}).parse();
|
||||
|
||||
|
||||
return notifications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves unseen notifications count.
|
||||
* @returns {Promise.<number>} unseen notifications count.
|
||||
* @returns {Promise.<number>}
|
||||
*/
|
||||
async getUnseenNotificationsCount() {
|
||||
const response = await Actions.notifications(this, 'get_unseen_count');
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not get unseen notifications count', response);
|
||||
const response = await this.actions.notifications('get_unseen_count');
|
||||
return response.data.unseenCount;
|
||||
}
|
||||
|
||||
@@ -557,40 +364,41 @@ class Innertube {
|
||||
* Retrieves lyrics for a given song if available.
|
||||
*
|
||||
* @param {string} video_id
|
||||
* @returns {Promise.<string>} Song lyrics
|
||||
* @returns {Promise.<string>}
|
||||
*/
|
||||
async getLyrics(video_id) {
|
||||
const continuation = await Actions.next(this, { video_id: video_id, ytmusic: true });
|
||||
if (!continuation.success) throw new Utils.InnertubeError('Could not retrieve lyrics', continuation);
|
||||
Utils.throwIfMissing({ video_id });
|
||||
|
||||
const continuation = await this.actions.next({ video_id: video_id, client: 'YTMUSIC' });
|
||||
const lyrics_tab = Utils.findNode(continuation, 'contents', 'Lyrics', 8, false);
|
||||
|
||||
const response = await this.actions.browse(lyrics_tab.endpoint?.browseEndpoint.browseId, { client: 'YTMUSIC' });
|
||||
if (!response.data?.contents?.sectionListRenderer) throw new Utils.UnavailableContentError('Lyrics not available', { video_id });
|
||||
|
||||
const lyrics_tab = Utils.findNode(continuation, 'contents', 'Lyrics', 8, false);
|
||||
|
||||
const response = await Actions.browse(this, 'lyrics', { ytmusic: true, browse_id: lyrics_tab.endpoint?.browseEndpoint.browseId });
|
||||
if (!response.success || !response.data?.contents?.sectionListRenderer) throw new Utils.UnavailableContentError('Lyrics not available', { video_id });
|
||||
|
||||
const lyrics = Utils.findNode(response.data, 'contents', 'runs', 6, false);
|
||||
return lyrics.runs[0].text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a given playlist.
|
||||
* Retrieves the contents of a given playlist.
|
||||
*
|
||||
* @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`
|
||||
*
|
||||
* @param {string} playlist_id - The id of the playlist.
|
||||
* @param {object} options - { client: 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: [] }>}
|
||||
*/
|
||||
async getPlaylist(playlist_id, options = { client: 'YOUTUBE' }) {
|
||||
const response = await Actions.browse(this, options.client == 'YTMUSIC' ? 'music_playlist' : 'playlist', { ytmusic: options.client == 'YTMUSIC', browse_id: `VL${playlist_id}` });
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not get playlist', response);
|
||||
Utils.throwIfMissing({ playlist_id });
|
||||
|
||||
const response = await this.actions.browse(`VL${playlist_id}`, { client: options.client });
|
||||
const playlist = new Parser(this, response.data, {
|
||||
client: options.client,
|
||||
client: options.client,
|
||||
data_type: 'PLAYLIST'
|
||||
}).parse();
|
||||
|
||||
|
||||
return playlist;
|
||||
}
|
||||
|
||||
@@ -599,6 +407,7 @@ class Innertube {
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {object} video_data
|
||||
*
|
||||
* @returns {object.<{ selected_format: {}; formats: [] }>}
|
||||
*/
|
||||
#chooseFormat(options, video_data) {
|
||||
@@ -612,7 +421,7 @@ class Innertube {
|
||||
format.url = format.url || format.signatureCipher || format.cipher;
|
||||
|
||||
if (format.signatureCipher || format.cipher) {
|
||||
format.url = new Signature(format.url, this.#player).decipher();
|
||||
format.url = new Signature(format.url, this.#player.signature_decipher).decipher();
|
||||
}
|
||||
|
||||
const url_components = new URL(format.url);
|
||||
@@ -620,7 +429,7 @@ class Innertube {
|
||||
url_components.searchParams.set('ratebypass', 'yes');
|
||||
|
||||
if (url_components.searchParams.get('n')) {
|
||||
url_components.searchParams.set('n', new NToken(this.#player.ntoken_sc, url_components.searchParams.get('n')).transform());
|
||||
url_components.searchParams.set('n', new NToken(this.#player.ntoken_decipher, url_components.searchParams.get('n')).transform());
|
||||
}
|
||||
|
||||
format.url = url_components.toString();
|
||||
@@ -631,9 +440,6 @@ class Innertube {
|
||||
delete format.signatureCipher;
|
||||
});
|
||||
|
||||
formats.hls_manifest_url = video_data.streamingData.hlsManifestUrl || undefined;
|
||||
formats.dash_manifest_url = video_data.streamingData.dashManifestUrl || undefined;
|
||||
|
||||
let format;
|
||||
let bitrates;
|
||||
let filtered_formats;
|
||||
@@ -644,21 +450,17 @@ class Innertube {
|
||||
'videoandaudio': formats.filter((format) => format.has_video && format.has_audio)
|
||||
})[options.type] || formats.filter((format) => format.has_video && format.has_audio);
|
||||
|
||||
if (options.type != 'videoandaudio') {
|
||||
let streams;
|
||||
let streams;
|
||||
|
||||
options.type != 'audio' &&
|
||||
(streams = filtered_formats.filter((format) => format.mimeType.includes(options.format || 'mp4') && format.qualityLabel == options.quality)) ||
|
||||
(streams = filtered_formats.filter((format) => format.mimeType.includes(options.format || 'mp4')));
|
||||
options.type != 'audio' &&
|
||||
(streams = filtered_formats.filter((format) => format.mimeType.includes(options.format || 'mp4') && format.qualityLabel == options.quality)) ||
|
||||
(streams = filtered_formats.filter((format) => format.mimeType.includes(options.format || 'mp4')));
|
||||
|
||||
streams == undefined || streams.length == 0 &&
|
||||
(streams = filtered_formats.filter((format) => format.quality == 'medium'));
|
||||
!streams || !streams.length &&
|
||||
(streams = filtered_formats.filter((format) => format.quality == 'medium'));
|
||||
|
||||
bitrates = streams.map((format) => format.bitrate);
|
||||
format = streams.filter((format) => format.bitrate === Math.max(...bitrates))[0];
|
||||
} else {
|
||||
format = filtered_formats[0];
|
||||
}
|
||||
bitrates = streams.map((format) => format.bitrate);
|
||||
format = streams.filter((format) => format.bitrate === Math.max(...bitrates))[0];
|
||||
|
||||
return { selected_format: format, formats };
|
||||
}
|
||||
@@ -667,21 +469,26 @@ class Innertube {
|
||||
* An alternative to {@link download}.
|
||||
* Returns deciphered streaming data.
|
||||
*
|
||||
* @param {string} id - Video id
|
||||
* @param {object} options - Download options.
|
||||
* @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
|
||||
* @returns {Promise.<{ selected_format: {}; formats: [] }>}
|
||||
* @param {string} video_id - video id
|
||||
* @param {object} options - download options.
|
||||
* @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
|
||||
*
|
||||
* @returns {Promise.<{ selected_format: object; formats: object[] }>}
|
||||
*/
|
||||
async getStreamingData(id, options = {}) {
|
||||
async getStreamingData(video_id, options = {}) {
|
||||
Utils.throwIfMissing({ video_id });
|
||||
|
||||
options.quality = options.quality || '360p';
|
||||
options.type = options.type || 'videoandaudio';
|
||||
options.format = options.format || 'mp4';
|
||||
|
||||
const data = await Actions.getVideoInfo(this, { id });
|
||||
const data = await this.actions.getVideoInfo(video_id);
|
||||
const streaming_data = this.#chooseFormat(options, data);
|
||||
if (!streaming_data.selected_format) throw new Utils.NoStreamingDataError('Could not find any suitable format.', { id, options });
|
||||
|
||||
if (!streaming_data.selected_format)
|
||||
throw new Utils.NoStreamingDataError('Could not find any suitable format.', { video_id, options });
|
||||
|
||||
return streaming_data;
|
||||
}
|
||||
@@ -689,25 +496,31 @@ class Innertube {
|
||||
/**
|
||||
* Downloads a given video. If you only need the direct download link take a look at {@link getStreamingData}.
|
||||
*
|
||||
* @param {string} id - Video id
|
||||
* @param {object} options - Download options.
|
||||
* @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
|
||||
* @return {ReadableStream}
|
||||
* @param {string} video_id - video id
|
||||
* @param {object} options - download options.
|
||||
* @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}
|
||||
*/
|
||||
download(id, options = {}) {
|
||||
if (!id) throw new Utils.MissingParamError('Video id is missing');
|
||||
|
||||
download(video_id, options = {}) {
|
||||
Utils.throwIfMissing({ video_id });
|
||||
|
||||
options.quality = options.quality || '360p';
|
||||
options.type = options.type || 'videoandaudio';
|
||||
options.format = options.format || 'mp4';
|
||||
|
||||
let cancel;
|
||||
let cancelled = false;
|
||||
|
||||
|
||||
const cpn = Utils.generateRandomString(16);
|
||||
|
||||
const stream = new Stream.PassThrough();
|
||||
Actions.getVideoInfo(this, { id }).then(async (video_data) => {
|
||||
this.actions.getVideoInfo(video_id, cpn).then(async (video_data) => {
|
||||
if (video_data.playabilityStatus.status === 'LOGIN_REQUIRED')
|
||||
return stream.emit('error', { message: 'You must login to download age-restricted videos.', error_type: 'LOGIN_REQUIRED', playability_status: video_data.playabilityStatus.status });
|
||||
if (!video_data.streamingData)
|
||||
@@ -722,7 +535,7 @@ class Innertube {
|
||||
stream.emit('info', { video_details, selected_format: format, formats });
|
||||
|
||||
if (options.type == 'videoandaudio' && !options.range) {
|
||||
const response = await Axios.get(format.url, {
|
||||
const response = await Axios.get(`${format.url}&cpn=${cpn}`, {
|
||||
responseType: 'stream',
|
||||
cancelToken: new CancelToken(function executor(c) { cancel = c; }),
|
||||
headers: Constants.STREAM_HEADERS
|
||||
@@ -771,12 +584,12 @@ 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);
|
||||
|
||||
const response = await Axios.get(`${format.url}&range=${chunk_start}-${chunk_end || ''}`, {
|
||||
|
||||
const response = await Axios.get(`${format.url}&cpn=${cpn}&range=${chunk_start}-${chunk_end || ''}`, {
|
||||
responseType: 'stream',
|
||||
cancelToken: new CancelToken(function executor(c) { cancel = c; }),
|
||||
headers: Constants.STREAM_HEADERS
|
||||
|
||||
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;
|
||||
File diff suppressed because it is too large
Load Diff
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;
|
||||
@@ -1,6 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const Actions = require('./Actions');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
class Livechat extends EventEmitter {
|
||||
@@ -27,7 +26,7 @@ class Livechat extends EventEmitter {
|
||||
async #poll() {
|
||||
if (!this.running) return;
|
||||
|
||||
const livechat = await Actions.livechat(this.session, 'live_chat/get_live_chat', { ctoken: this.ctoken });
|
||||
const livechat = await this.session.actions.livechat('live_chat/get_live_chat', { ctoken: this.ctoken });
|
||||
if (!livechat.success) {
|
||||
this.emit('error', { message: `Failed polling livechat: ${livechat.message}. Retrying...` });
|
||||
return await this.#poll();
|
||||
@@ -46,9 +45,9 @@ class Livechat extends EventEmitter {
|
||||
this.message_queue = [];
|
||||
|
||||
const data = { video_id: this.video_id };
|
||||
if (this.metadata_ctoken) data.continuation = this.metadata_ctoken;
|
||||
if (this.metadata_ctoken) data.ctoken = this.metadata_ctoken;
|
||||
|
||||
const updated_metadata = await Actions.livechat(this.session, 'updated_metadata', data);
|
||||
const updated_metadata = await this.session.actions.livechat('updated_metadata', data);
|
||||
if (!updated_metadata.success) {
|
||||
this.emit('error', { message: `Failed polling livechat metadata: ${livechat.message}.` });
|
||||
}
|
||||
@@ -89,16 +88,16 @@ class Livechat extends EventEmitter {
|
||||
}
|
||||
|
||||
async sendMessage(text) {
|
||||
const message = await Actions.livechat(this.session, 'live_chat/send_message', { text, channel_id: this.channel_id, video_id: this.video_id });
|
||||
const message = await this.session.actions.livechat('live_chat/send_message', { text, channel_id: this.channel_id, video_id: this.video_id });
|
||||
if (!message.success) return message;
|
||||
|
||||
const deleteMessage = async () => {
|
||||
const menu = await Actions.livechat(this.session, 'live_chat/get_item_context_menu', { params: { params: message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.contextMenuEndpoint.liveChatItemContextMenuEndpoint.params, pbj: 1 } });
|
||||
const menu = await this.session.actions.livechat('live_chat/get_item_context_menu', { params: { params: message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.contextMenuEndpoint.liveChatItemContextMenuEndpoint.params, pbj: 1 } });
|
||||
if (!menu.success) return menu;
|
||||
|
||||
const chat_item_menu = menu.data.liveChatItemContextMenuSupportedRenderers.menuRenderer.items[0];
|
||||
|
||||
const cmd = await Actions.livechat(this.session, 'live_chat/moderate', { cmd_params: chat_item_menu.menuServiceItemRenderer.serviceEndpoint.moderateLiveChatEndpoint.params });
|
||||
const cmd = await this.session.actions.livechat('live_chat/moderate', { params: chat_item_menu.menuServiceItemRenderer.serviceEndpoint.moderateLiveChatEndpoint.params });
|
||||
if (!cmd.success) return cmd;
|
||||
|
||||
return { success: true, status_code: cmd.status_code };
|
||||
@@ -123,10 +122,9 @@ class Livechat extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Blocks a user.
|
||||
* @todo Implement this method.
|
||||
* @param {object} msg_params
|
||||
* @todo Implement this method
|
||||
*/
|
||||
async blockUser(msg_params) {
|
||||
async blockUser() {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
|
||||
@@ -4,19 +4,20 @@ 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`;
|
||||
|
||||
#auth_info = {};
|
||||
#refresh_interval = 5;
|
||||
#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);
|
||||
@@ -61,7 +62,7 @@ class OAuth {
|
||||
verification_url: response.data.verification_url
|
||||
});
|
||||
|
||||
this.refresh_interval = response.data.interval;
|
||||
this.#polling_interval = response.data.interval;
|
||||
|
||||
this.#waitForAuth(response.data.device_code);
|
||||
}
|
||||
@@ -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 () => {
|
||||
@@ -121,7 +122,7 @@ class OAuth {
|
||||
status: 'SUCCESS'
|
||||
});
|
||||
}
|
||||
}, 1000 * this.#refresh_interval);
|
||||
}, 1000 * this.#polling_interval);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -197,23 +198,31 @@ class OAuth {
|
||||
const url_body = Constants.OAUTH.REGEX.AUTH_SCRIPT.exec(yttv_response.data)[1];
|
||||
const script_url = `${Constants.URLS.YT_BASE}/${url_body}`;
|
||||
|
||||
const response = await Axios.get(script_url, Constants.DEFAULT_HEADERS()).catch((error) => error);
|
||||
const response = await Axios.get(script_url).catch((error) => error);
|
||||
if (response instanceof Error) throw new Error(`Could not extract client identity: ${response.message}`);
|
||||
|
||||
const client_identity = response.data.replace(/\n/g, '').match(Constants.OAUTH.REGEX.CLIENT_IDENTITY);
|
||||
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,49 +1,127 @@
|
||||
'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 {
|
||||
constructor(session) {
|
||||
this.session = session;
|
||||
this.player_name = Utils.getStringBetweenStrings(this.session.player_url, '/player/', '/');
|
||||
this.tmp_cache_dir = __dirname.slice(0, -8) + 'cache';
|
||||
#player_id;
|
||||
#player_url;
|
||||
#player_path;
|
||||
|
||||
#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 = `${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`;
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (Fs.existsSync(`${this.tmp_cache_dir}/${this.player_name}.js`)) {
|
||||
const player_data = Fs.readFileSync(`${this.tmp_cache_dir}/${this.player_name}.js`).toString();
|
||||
this.sig_decipher_sc = this.#getSigDecipherCode(player_data);
|
||||
this.ntoken_sc = this.#getNEncoder(player_data);
|
||||
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);
|
||||
} else {
|
||||
const response = await Axios.get(`${Constants.URLS.YT_BASE}${this.session.player_url}`, { path: this.session.playerUrl, headers: { 'content-type': 'text/javascript', 'user-agent': Utils.getRandomUserAgent('desktop').userAgent } }).catch((error) => error);
|
||||
if (response instanceof Error) throw new Error('Could not download player script: ' + response.message);
|
||||
|
||||
const response = await Axios.get(this.#player_url, { headers: { 'content-type': 'text/javascript', 'user-agent': Utils.getRandomUserAgent('desktop').userAgent } }).catch((error) => error);
|
||||
if (response instanceof Error) throw new Utils.InnertubeError('Could not download js player', { player_id: this.#player_id });
|
||||
|
||||
this.#signature_timestamp = this.#extractSigTimestamp(response.data);
|
||||
this.#signature_decipher_sc = this.#extractSigDecipherSc(response.data);
|
||||
this.#ntoken_decipher_sc = this.#extractNTokenSc(response.data);
|
||||
|
||||
try {
|
||||
// Deletes old players
|
||||
Fs.existsSync(this.tmp_cache_dir) && Fs.rmSync(this.tmp_cache_dir, { recursive: true });
|
||||
|
||||
// Caches the current player so we don't have to download it all the time.
|
||||
Fs.mkdirSync(this.tmp_cache_dir, { recursive: true });
|
||||
Fs.writeFileSync(`${this.tmp_cache_dir}/${this.player_name}.js`, response.data);
|
||||
} catch (err) {}
|
||||
|
||||
this.sig_decipher_sc = this.#getSigDecipherCode(response.data);
|
||||
this.ntoken_sc = this.#getNEncoder(response.data);
|
||||
// Delete the old player
|
||||
Fs.existsSync(this.#cache_dir) &&
|
||||
Fs.rmSync(this.#cache_dir, { recursive: true });
|
||||
|
||||
// Cache the current player
|
||||
Fs.mkdirSync(this.#cache_dir, { recursive: true });
|
||||
Fs.writeFileSync(this.#player_path, response.data);
|
||||
} finally { /* do nothing */ }
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
#getSigDecipherCode(data) {
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
#getNEncoder(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("");`;
|
||||
}
|
||||
|
||||
isCached() {
|
||||
return Fs.existsSync(this.#player_path);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Player;
|
||||
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;
|
||||
146
lib/core/SessionBuilder.js
Normal file
146
lib/core/SessionBuilder.js
Normal file
@@ -0,0 +1,146 @@
|
||||
'use strict';
|
||||
|
||||
const Axios = require('axios');
|
||||
const Player = require('./Player');
|
||||
const Proto = require('../proto');
|
||||
const Utils = require('../utils/Utils');
|
||||
const Constants = require('../utils/Constants');
|
||||
const UserAgent = require('user-agents');
|
||||
|
||||
/** @namespace */
|
||||
class SessionBuilder {
|
||||
#config;
|
||||
|
||||
#key;
|
||||
#client_name;
|
||||
#client_version;
|
||||
#api_version;
|
||||
#remote_host;
|
||||
#context;
|
||||
#player;
|
||||
|
||||
/**
|
||||
* @param {object} config
|
||||
* @constructor
|
||||
*/
|
||||
constructor(config) {
|
||||
this.#config = config;
|
||||
}
|
||||
|
||||
async build() {
|
||||
const data = await Promise.all([
|
||||
this.#getYtConfig(),
|
||||
this.#getPlayerId()
|
||||
]);
|
||||
|
||||
const ytcfg = data[0][0][2];
|
||||
|
||||
this.#key = ytcfg[1];
|
||||
this.#api_version = `v${ytcfg[0][0][6]}`;
|
||||
this.#client_name = Constants.CLIENTS.WEB.NAME;
|
||||
this.#client_version = ytcfg[0][0][16];
|
||||
this.#remote_host = ytcfg[0][0][3];
|
||||
this.#player = await new Player(data[1]).init();
|
||||
|
||||
this.#context = this.#buildContext();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a valid context object.
|
||||
* @returns
|
||||
*/
|
||||
#buildContext() {
|
||||
const user_agent = new UserAgent({ deviceCategory: 'desktop' });
|
||||
|
||||
const id = Utils.generateRandomString(11);
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
const visitor_data = Proto.encodeVisitorData(id, timestamp);
|
||||
|
||||
const context = {
|
||||
client: {
|
||||
hl: 'en',
|
||||
gl: this.#config.gl || 'US',
|
||||
remoteHost: this.#remote_host,
|
||||
deviceMake: user_agent.vendor,
|
||||
deviceModel: user_agent.platform,
|
||||
visitorData: visitor_data,
|
||||
userAgent: user_agent.toString(),
|
||||
clientName: this.#client_name,
|
||||
clientVersion: this.#client_version,
|
||||
originalUrl: Constants.URLS.YT_BASE
|
||||
},
|
||||
user: { lockedSafetyMode: false },
|
||||
request: { useSsl: true }
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (response instanceof Error)
|
||||
throw new Utils.InnertubeError('Could not retrieve configuration data', {
|
||||
status_code: response?.response?.status || 0,
|
||||
message: response.message
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
if (response instanceof Error)
|
||||
throw new Utils.InnertubeError('Could not retrieve js player id', {
|
||||
status_code: response?.response?.status || 0,
|
||||
message: response.message
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SessionBuilder;
|
||||
@@ -11,7 +11,7 @@ class NToken {
|
||||
|
||||
/**
|
||||
* Solves throttling challange by transforming the n token.
|
||||
* @returns {string} transformed token.
|
||||
* @returns {string}
|
||||
*/
|
||||
transform() {
|
||||
let n_token = this.n.split('');
|
||||
@@ -52,19 +52,22 @@ class NToken {
|
||||
transformations[data.index](transformations[param_index[0]], transformations[param_index[1]], base64_dia);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`Could not transform n-token (${this.n}), download may be throttled:`, err.message);
|
||||
console.error(new Utils.ParsingError('Could not transform n-token, download may be throttled.', {
|
||||
original_token: this.n,
|
||||
stack: err.stack
|
||||
}));
|
||||
return this.n;
|
||||
}
|
||||
return n_token.join('');
|
||||
}
|
||||
|
||||
#getFunc(el) {
|
||||
return el.match(Constants.FUNCS_REGEX);
|
||||
return el.match(Constants.NTOKEN_REGEX.FUNCTIONS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the n-transform data, refines it, and then returns a readable json array.
|
||||
* @returns {object}
|
||||
* @returns {Array}
|
||||
*/
|
||||
#getTransformationData() {
|
||||
const data = `[${Utils.getStringBetweenStrings(this.raw_code.replace(/\n/g, ''), 'c=[', '];c')}]`;
|
||||
|
||||
@@ -1,74 +1,90 @@
|
||||
'use strict';
|
||||
|
||||
const Constants = require('../utils/Constants');
|
||||
const QueryString = require('querystring');
|
||||
|
||||
class Signature {
|
||||
constructor(url, player) {
|
||||
constructor(url, sig_decipher_sc) {
|
||||
this.url = url;
|
||||
this.player = player;
|
||||
this.func_regex = /(.{2}):function\(.*?\){(.*?)}/g;
|
||||
this.actions_regex = /;.{2}\.(.{2})\(.*?,(.*?)\)/g;
|
||||
this.sig_decipher_sc = sig_decipher_sc;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deciphers signature.
|
||||
* @returns {string}
|
||||
*/
|
||||
decipher() {
|
||||
let actions;
|
||||
|
||||
const args = QueryString.parse(this.url);
|
||||
const signature = args.s.split('');
|
||||
|
||||
const functions = this.#getFunctions();
|
||||
|
||||
function splice(arr, end) {
|
||||
arr.splice(0, end);
|
||||
}
|
||||
|
||||
function swap(arr, index) {
|
||||
let origArrI = arr[0];
|
||||
arr[0] = arr[index % arr.length];
|
||||
arr[index % arr.length] = origArrI;
|
||||
}
|
||||
|
||||
function reverse(arr) {
|
||||
arr.reverse();
|
||||
}
|
||||
|
||||
let actions;
|
||||
let signature = args.s.split('');
|
||||
|
||||
while ((actions = this.actions_regex.exec(this.player.sig_decipher_sc)) !== null) {
|
||||
switch (actions[1]) {
|
||||
/**
|
||||
* Decides what function should be used to modify the
|
||||
* the signature.
|
||||
*/
|
||||
while ((actions = Constants.SIG_REGEX.ACTIONS.exec(this.sig_decipher_sc)) !== null) {
|
||||
const action = actions.groups;
|
||||
switch (action.name) {
|
||||
case functions[0]:
|
||||
reverse(signature, actions[2]);
|
||||
this.#reverse(signature);
|
||||
break;
|
||||
case functions[1]:
|
||||
splice(signature, actions[2]);
|
||||
this.#splice(signature, action.param);
|
||||
break;
|
||||
case functions[2]:
|
||||
swap(signature, actions[2]);
|
||||
this.#swap(signature, action.param);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
const url_components = new URL(args.url);
|
||||
args.sp ? url_components.searchParams.set(args.sp, signature.join('')) : url_components.searchParams.set('signature', signature.join(''));
|
||||
|
||||
args.sp ?
|
||||
url_components.searchParams.set(args.sp, signature.join('')) :
|
||||
url_components.searchParams.set('signature', signature.join(''));
|
||||
|
||||
return url_components.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extracts the functions used to modify the signature
|
||||
* and returns them in the correct order.
|
||||
*
|
||||
* @returns {Array.<string>}
|
||||
*/
|
||||
#getFunctions() {
|
||||
let func;
|
||||
let func_name = [];
|
||||
|
||||
while ((func = this.func_regex.exec(this.player.sig_decipher_sc)) !== null) {
|
||||
if (func[0].includes('reverse()')) {
|
||||
func_name[0] = func[1];
|
||||
let functions = [];
|
||||
|
||||
while ((func = Constants.SIG_REGEX.FUNCTIONS.exec(this.sig_decipher_sc)) !== null) {
|
||||
if (func[0].includes('reverse')) {
|
||||
functions[0] = func[1];
|
||||
} else if (func[0].includes('splice')) {
|
||||
func_name[1] = func[1];
|
||||
functions[1] = func[1];
|
||||
} else {
|
||||
func_name[2] = func[1];
|
||||
functions[2] = func[1];
|
||||
}
|
||||
}
|
||||
|
||||
return func_name;
|
||||
|
||||
return functions;
|
||||
}
|
||||
|
||||
#swap(arr, index) {
|
||||
let origArrI = arr[0];
|
||||
arr[0] = arr[index % arr.length];
|
||||
arr[index % arr.length] = origArrI;
|
||||
}
|
||||
|
||||
#splice(arr, end) {
|
||||
arr.splice(0, end);
|
||||
}
|
||||
|
||||
#reverse(arr) {
|
||||
arr.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const Utils = require('../utils/Utils');
|
||||
const Actions = require('../core/Actions');
|
||||
const Constants = require('../utils/Constants');
|
||||
const YTDataItems = require('./youtube');
|
||||
const YTMusicDataItems = require('./ytmusic');
|
||||
@@ -28,6 +27,7 @@ class Parser {
|
||||
PLAYLIST: () => this.#processPlaylist(),
|
||||
SUBSFEED: () => this.#processSubscriptionFeed(),
|
||||
HOMEFEED: () => this.#processHomeFeed(),
|
||||
LIBRARY: () => this.#processLibrary(), //WIP
|
||||
TRENDING: () => this.#processTrending(),
|
||||
HISTORY: () => this.#processHistory(),
|
||||
COMMENTS: () => this.#processComments(),
|
||||
@@ -68,9 +68,8 @@ class Parser {
|
||||
const citem = contents.find((item) => item.continuationItemRenderer);
|
||||
const ctoken = citem.continuationItemRenderer.continuationEndpoint.continuationCommand.token;
|
||||
|
||||
const response = await Actions.search(this.session, 'YOUTUBE', { ctoken });
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not get continuation', response);
|
||||
|
||||
const response = await this.session.actions.search({ ctoken });
|
||||
|
||||
const continuation_items = Utils.findNode(response.data, 'onResponseReceivedCommands', 'itemSectionRenderer', 4, false);
|
||||
return parseItems(continuation_items);
|
||||
};
|
||||
@@ -84,7 +83,7 @@ class Parser {
|
||||
#processMusicSearch() {
|
||||
const tabs = Utils.findNode(this.data, 'contents', 'tabs').tabs;
|
||||
const contents = Utils.findNode(tabs, '0', 'contents', 5);
|
||||
|
||||
|
||||
const did_you_mean_item = contents.find((content) => content.itemSectionRenderer);
|
||||
const did_you_mean_renderer = did_you_mean_item?.itemSectionRenderer.contents[0].didYouMeanRenderer;
|
||||
|
||||
@@ -110,7 +109,7 @@ class Parser {
|
||||
['Community playlists']: () => YTMusicDataItems.PlaylistResultItem.parse(section.contents),
|
||||
['Artists']: () => YTMusicDataItems.ArtistResultItem.parse(section.contents),
|
||||
['Albums']: () => YTMusicDataItems.AlbumResultItem.parse(section.contents)
|
||||
})[section_title]();
|
||||
}[section_title] || (() => {}))();
|
||||
|
||||
processed_data.results[section_title.replace(/ /g, '_').toLowerCase()] = section_items;
|
||||
}
|
||||
@@ -120,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() {
|
||||
@@ -130,12 +129,12 @@ class Parser {
|
||||
|
||||
#processPlaylist() {
|
||||
const details = this.data.sidebar.playlistSidebarRenderer.items[0];
|
||||
|
||||
|
||||
const metadata = {
|
||||
title: this.data.metadata.playlistMetadataRenderer.title,
|
||||
description: details.playlistSidebarPrimaryInfoRenderer.description.simpleText || 'N/A',
|
||||
total_items: details.playlistSidebarPrimaryInfoRenderer.stats[0].runs[0].text,
|
||||
last_updated: details.playlistSidebarPrimaryInfoRenderer.stats[2].runs[1].text,
|
||||
description: details.playlistSidebarPrimaryInfoRenderer?.description?.simpleText || 'N/A',
|
||||
total_items: details.playlistSidebarPrimaryInfoRenderer.stats[0].runs[0]?.text || 'N/A',
|
||||
last_updated: details.playlistSidebarPrimaryInfoRenderer.stats[2].runs[1]?.text || 'N/A',
|
||||
views: details.playlistSidebarPrimaryInfoRenderer.stats[1].simpleText
|
||||
}
|
||||
|
||||
@@ -290,16 +289,16 @@ class Parser {
|
||||
response.items = items.map((item) => {
|
||||
const comment = YTDataItems.CommentThread.parseItem(item);
|
||||
if (comment) {
|
||||
comment.like = () => Actions.engage(this.session, 'comment/perform_comment_action', { comment_action: 'like', comment_id: comment.metadata.id, video_id: this.args.video_id }),
|
||||
comment.dislike = () => Actions.engage(this.session, 'comment/perform_comment_action', { comment_action: 'dislike', comment_id: comment.metadata.id, video_id: this.args.video_id }),
|
||||
comment.reply = (text) => Actions.engage(this.session, 'comment/create_comment_reply', { text, comment_id: comment.metadata.id, video_id: this.args.video_id });
|
||||
comment.like = () => this.session.actions.engage('comment/perform_comment_action', { comment_action: 'like', comment_id: comment.metadata.id, video_id: this.args.video_id });
|
||||
comment.dislike = () => this.session.actions.engage('comment/perform_comment_action', { comment_action: 'dislike', comment_id: comment.metadata.id, video_id: this.args.video_id });
|
||||
comment.reply = (text) => this.session.actions.engage('comment/create_comment_reply', { text, comment_id: comment.metadata.id, video_id: this.args.video_id });
|
||||
|
||||
comment.report = async () => {
|
||||
const payload = Utils.findNode(item, 'commentThreadRenderer', 'params', 10, false);
|
||||
const form = await Actions.flag(this.session, 'flag/get_form', { params: payload.params });
|
||||
const form = await this.session.actions.flag('flag/get_form', { params: payload.params });
|
||||
|
||||
const action = Utils.findNode(form, 'actions', 'flagAction', 13, false);
|
||||
const flag = await Actions.flag(this.session, 'flag/flag', { action: action.flagAction });
|
||||
const flag = await this.session.actions.flag('flag/flag', { action: action.flagAction });
|
||||
|
||||
return flag;
|
||||
};
|
||||
@@ -307,21 +306,41 @@ class Parser {
|
||||
comment.getReplies = async () => {
|
||||
if (comment.metadata.reply_count === 0) throw new Utils.InnertubeError('This comment has no replies', comment);
|
||||
const payload = Proto.encodeCommentRepliesParams(this.args.video_id, comment.metadata.id);
|
||||
const next = await Actions.next(this.session, { continuation_token: payload });
|
||||
const next = await this.session.actions.next({ ctoken: payload });
|
||||
return parseComments(next.data);
|
||||
};
|
||||
|
||||
comment.translate = async (target_language) => {
|
||||
const response = await this.session.actions.engage('comment/perform_comment_action', {
|
||||
text: comment.text,
|
||||
comment_action: 'translate',
|
||||
comment_id: comment.metadata.id,
|
||||
video_id: this.args.video_id,
|
||||
target_language
|
||||
});
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return comment;
|
||||
}
|
||||
}).filter((c) => c);
|
||||
|
||||
response.comment = (text) => this.session.actions.engage('comment/create_comment', { video_id: this.args.video_id, text });
|
||||
|
||||
response.getContinuation = async () => {
|
||||
const continuation_item = items.find((item) => item.continuationItemRenderer);
|
||||
if (!continuation_item) throw new Utils.InnertubeError('You\'ve reached the end');
|
||||
|
||||
const is_reply = !!continuation_item.continuationItemRenderer.button;
|
||||
const payload = Utils.findNode(continuation_item, 'continuationItemRenderer', 'token', is_reply && 5 || 3);
|
||||
const next = await Actions.next(this.session, { continuation_token: payload.token });
|
||||
const next = await this.session.actions.next({ ctoken: payload.token });
|
||||
|
||||
return parseComments(next.data);
|
||||
};
|
||||
@@ -342,9 +361,8 @@ class Parser {
|
||||
const citem = contents.find((item) => item.continuationItemRenderer);
|
||||
const ctoken = citem.continuationItemRenderer.continuationEndpoint.continuationCommand.token;
|
||||
|
||||
const response = await Actions.browse(this.session, 'continuation', { ctoken });
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve continuation', response);
|
||||
|
||||
const response = await this.session.actions.browse(ctoken, { is_ctoken: true });
|
||||
|
||||
return parseItems(response.data.onResponseReceivedActions[0].appendContinuationItemsAction.continuationItems);
|
||||
}
|
||||
|
||||
@@ -354,6 +372,33 @@ class Parser {
|
||||
return parseItems(contents);
|
||||
}
|
||||
|
||||
#processLibrary() { // TODO: Finish this
|
||||
const profile_data = Utils.findNode(this.data, 'contents', 'profileColumnRenderer', 3);
|
||||
const stats_data = profile_data.profileColumnRenderer.items.find((item) => item.profileColumnStatsRenderer);
|
||||
const stats_items = stats_data.profileColumnStatsRenderer.items;
|
||||
const userinfo = profile_data.profileColumnRenderer.items.find((item) => item.profileColumnUserInfoRenderer);
|
||||
|
||||
const stats = {};
|
||||
|
||||
stats_items.forEach((item) => {
|
||||
const label = item.profileColumnStatsEntryRenderer.label.runs.map((run) => run.text).join('');
|
||||
stats[label.toLowerCase()] = parseInt(item.profileColumnStatsEntryRenderer.value.simpleText);
|
||||
});
|
||||
|
||||
const profile = {
|
||||
name: userinfo.profileColumnUserInfoRenderer?.title?.simpleText,
|
||||
thumbnails: userinfo.profileColumnUserInfoRenderer?.thumbnail.thumbnails,
|
||||
stats
|
||||
}
|
||||
|
||||
// const content = Utils.findNode(this.data, 'contents', 'content', 8, false);
|
||||
// console.info(content[0].itemSectionRenderer.contents[0].shelfRenderer);
|
||||
|
||||
return {
|
||||
profile
|
||||
}
|
||||
}
|
||||
|
||||
#processSubscriptionFeed() {
|
||||
const contents = Utils.findNode(this.data, 'contents', 'contents', 9, false);
|
||||
|
||||
@@ -379,9 +424,8 @@ class Parser {
|
||||
const citem = contents.find((item) => item.continuationItemRenderer);
|
||||
const ctoken = citem.continuationItemRenderer.continuationEndpoint.continuationCommand.token;
|
||||
|
||||
const response = await Actions.browse(this.session, 'continuation', { ctoken });
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve continuation', response);
|
||||
|
||||
const response = await this.session.actions.browse(ctoken, { is_ctoken: true });
|
||||
|
||||
const ccontents = Utils.findNode(response.data, 'onResponseReceivedActions', 'itemSectionRenderer', 4, false);
|
||||
subsfeed.items = [];
|
||||
|
||||
@@ -445,7 +489,7 @@ class Parser {
|
||||
|
||||
#processNotifications() {
|
||||
const contents = this.data.actions[0].openPopupAction.popup.multiPageMenuRenderer.sections[0];
|
||||
if (!contents.multiPageMenuNotificationSectionRenderer) throw new Utils.InnertubeError('No notifications', response);
|
||||
if (!contents.multiPageMenuNotificationSectionRenderer) throw new Utils.InnertubeError('No notifications');
|
||||
|
||||
const parseItems = (items) => {
|
||||
const parsed_items = YTDataItems.NotificationItem.parse(items);
|
||||
@@ -454,9 +498,8 @@ class Parser {
|
||||
const citem = items.find((item) => item.continuationItemRenderer);
|
||||
const ctoken = citem?.continuationItemRenderer?.continuationEndpoint?.getNotificationMenuEndpoint?.ctoken;
|
||||
|
||||
const response = await Actions.notifications(this.session, 'get_notification_menu', { ctoken });
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve continuation', response);
|
||||
|
||||
const response = await this.session.actions.notifications('get_notification_menu', { ctoken });
|
||||
|
||||
return parseItems(response.data.actions[0].appendContinuationItemsAction.continuationItems);
|
||||
}
|
||||
|
||||
@@ -470,7 +513,7 @@ class Parser {
|
||||
const tabs = Utils.findNode(this.data, 'contents', 'tabRenderer', 4, false);
|
||||
const categories = {};
|
||||
|
||||
const trending = tabs.map((tab) => {
|
||||
tabs.forEach((tab) => {
|
||||
const tab_renderer = tab.tabRenderer;
|
||||
const tab_content = tab_renderer?.content;
|
||||
const category_title = tab_renderer.title.toLowerCase();
|
||||
@@ -488,8 +531,7 @@ class Parser {
|
||||
} else { // The rest can only be fetched with additional calls
|
||||
const params = tab_renderer.endpoint.browseEndpoint.params;
|
||||
categories[category_title].getVideos = async () => {
|
||||
const response = await Actions.browse(this.session, 'trending', { params });
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve category videos', response);
|
||||
const response = await this.session.actions.browse('FEtrending', { params });
|
||||
|
||||
const tabs = Utils.findNode(response, 'contents', 'tabRenderer', 4, false);
|
||||
const tab = tabs.find((tab) => tab.tabRenderer.title === tab_renderer.title);
|
||||
@@ -530,9 +572,8 @@ class Parser {
|
||||
const citem = contents.find((item) => item.continuationItemRenderer);
|
||||
const ctoken = citem.continuationItemRenderer.continuationEndpoint.continuationCommand.token;
|
||||
|
||||
const response = await Actions.browse(this.session, 'continuation', { ctoken });
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve continuation', response);
|
||||
|
||||
const response = await this.session.actions.browse(ctoken, { is_ctoken: true });
|
||||
|
||||
history.items = [];
|
||||
|
||||
return parseItems(response.data.onResponseReceivedActions[0].appendContinuationItemsAction.continuationItems);
|
||||
|
||||
@@ -6,7 +6,6 @@ class CommentThread {
|
||||
static parseItem(item) {
|
||||
if (item.commentThreadRenderer || item.commentRenderer) {
|
||||
const comment = item?.commentThreadRenderer?.comment || item;
|
||||
const replies = item?.commentThreadRenderer?.replies;
|
||||
|
||||
const like_btn = comment.commentRenderer?.actionButtons?.commentActionButtonsRenderer.likeButton;
|
||||
const dislike_btn = comment.commentRenderer?.actionButtons?.commentActionButtonsRenderer.dislikeButton;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
class SearchSuggestionItem {
|
||||
static parse(data, bold_text) {
|
||||
return data.map((item) => ({
|
||||
text: item.trim(),
|
||||
bold_text: bold_text.trim().toLowerCase()
|
||||
}));
|
||||
static parse(data) {
|
||||
return {
|
||||
query: data[0],
|
||||
results: data[1].map((res) => res[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class VideoResultItem {
|
||||
channel: {
|
||||
id: renderer?.ownerText?.runs[0]?.navigationEndpoint?.browseEndpoint?.browseId,
|
||||
name: renderer?.ownerText?.runs[0]?.text,
|
||||
url: `${Constants.URLS.YT_BASE}${renderer.ownerText.runs[0].navigationEndpoint?.browseEndpoint?.canonicalBaseUrl}`
|
||||
url: `${Constants.URLS.YT_BASE}${renderer?.ownerText?.runs[0].navigationEndpoint?.browseEndpoint?.canonicalBaseUrl}`
|
||||
},
|
||||
metadata: {
|
||||
view_count: renderer?.viewCountText?.simpleText || 'N/A',
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
class MusicSearchSuggestionItem {
|
||||
static parse(data) {
|
||||
return data.map((item) => this.parseItem(item));
|
||||
return {
|
||||
query: this.parseItem(data[0]).runs[0].text.trim(),
|
||||
results: data.map((item) => this.parseItem(item).runs.map((run) => run.text).join('').trim())
|
||||
}
|
||||
}
|
||||
|
||||
static parseItem(item) {
|
||||
@@ -11,11 +14,8 @@ class MusicSearchSuggestionItem {
|
||||
item.historySuggestionRenderer &&
|
||||
(suggestion = item.historySuggestionRenderer.suggestion) ||
|
||||
(suggestion = item.searchSuggestionRenderer.suggestion);
|
||||
|
||||
return {
|
||||
text: suggestion.runs.map((run) => run.text).join('').trim(),
|
||||
bold_text: suggestion.runs[0].text.trim()
|
||||
};
|
||||
|
||||
return suggestion;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class TopResultItem {
|
||||
artist: () => ArtistResultItem.parseItem(item),
|
||||
album: () => AlbumResultItem.parseItem(item),
|
||||
single: () => AlbumResultItem.parseItem(item)
|
||||
}[type])();
|
||||
}[type] || (() => {}))();
|
||||
|
||||
parsed_item && (parsed_item.type = type);
|
||||
|
||||
|
||||
@@ -3,23 +3,112 @@
|
||||
const messages = require('./messages');
|
||||
|
||||
class Proto {
|
||||
static encodeSearchFilter(period, duration, order) {
|
||||
const periods = { 'any': null, 'hour': 1, 'day': 2, 'week': 3, 'month': 4, 'year': 5 };
|
||||
const durations = { 'any': null, 'short': 1, 'long': 2 };
|
||||
const orders = { 'relevance': null, 'rating': 1, 'age': 2, 'views': 3 };
|
||||
|
||||
const buf = messages.SearchFilter.encode({
|
||||
number: orders[order],
|
||||
filter: {
|
||||
param_0: periods[period],
|
||||
param_1: (period == 'hour' && order == 'relevance') ? null : 1,
|
||||
param_2: durations[duration]
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Encodes visitor data.
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {number} timestamp
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
static encodeVisitorData(id, timestamp) {
|
||||
const buf = messages.VisitorData.encode({ id, timestamp });
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64').replace(/\/|\+/g, '_'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes basic channel analytics parameters.
|
||||
*
|
||||
* @param {string} channel_id
|
||||
* @returns {string}
|
||||
*/
|
||||
static encodeChannelAnalyticsParams(channel_id) {
|
||||
const buf = messages.ChannelAnalytics.encode({ params: { channel_id } });
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes search filters.
|
||||
*
|
||||
* @param {object} filters
|
||||
* @param {string} [filters.upload_date] - any | last_hour | today | this_week | this_month | this_year
|
||||
* @param {string} [filters.type] - any | video | channel | playlist | movie
|
||||
* @param {string} [filters.duration] - any | short | medium | long
|
||||
* @param {string} [filters.sort_by] - relevance | rating | upload_date | view_count
|
||||
* @todo implement remaining filters.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
static encodeSearchFilter(filters) {
|
||||
const upload_dates = {
|
||||
'any': null,
|
||||
'last_hour': 1,
|
||||
'today': 2,
|
||||
'this_week': 3,
|
||||
'this_month': 4,
|
||||
'this_year': 5
|
||||
};
|
||||
|
||||
const types = {
|
||||
'any': null,
|
||||
'video': 1,
|
||||
'channel': 2,
|
||||
'playlist': 3,
|
||||
'movie': 4
|
||||
};
|
||||
|
||||
const durations = {
|
||||
'any': null,
|
||||
'short': 1,
|
||||
'medium': 2,
|
||||
'long': 3
|
||||
};
|
||||
|
||||
const orders = {
|
||||
'relevance': null,
|
||||
'rating': 1,
|
||||
'upload_date': 2,
|
||||
'view_count': 3
|
||||
};
|
||||
|
||||
const data = {};
|
||||
|
||||
filters &&
|
||||
(data.filters = {}) ||
|
||||
(data.no_filter = 0);
|
||||
|
||||
if (filters) {
|
||||
if (filters.upload_date) {
|
||||
if (!['video', 'movie'].includes(filters.type))
|
||||
throw new Error('Cannot use upload date filter with type ' + filters.type);
|
||||
}
|
||||
|
||||
filters.upload_date &&
|
||||
(data.filters.param_0 = upload_dates[filters.upload_date]);
|
||||
|
||||
filters.type &&
|
||||
(data.filters.param_1 = types[filters.type]);
|
||||
|
||||
filters.duration &&
|
||||
(data.filters.param_2 = durations[filters.duration]);
|
||||
|
||||
filters.sort_by &&
|
||||
(filters.sort_by !== 'relevance') &&
|
||||
(data.filter = orders[filters.sort_by]);
|
||||
}
|
||||
|
||||
const buf = messages.SearchFilter.encode(data);
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes livechat message parameters.
|
||||
*
|
||||
* @param {string} channel_id
|
||||
* @param {string} video_id
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
static encodeMessageParams(channel_id, video_id) {
|
||||
const buf = messages.LiveMessageParams.encode({
|
||||
params: { ids: { channel_id, video_id } },
|
||||
@@ -29,6 +118,16 @@ class Proto {
|
||||
return Buffer.from(encodeURIComponent(Buffer.from(buf).toString('base64'))).toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes comment section parameters.
|
||||
*
|
||||
* @param {string} video_id
|
||||
* @param {object} options
|
||||
* @param {string} options.type
|
||||
* @param {string} options.sort_by
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
static encodeCommentsSectionParams(video_id, options = {}) {
|
||||
const sort_menu = { TOP_COMMENTS: 0, NEWEST_FIRST: 1 };
|
||||
|
||||
@@ -48,6 +147,14 @@ class Proto {
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes replies thread parameters.
|
||||
*
|
||||
* @param {string} video_id
|
||||
* @param {string} comment_id
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
static encodeCommentRepliesParams(video_id, comment_id) {
|
||||
const buf = messages.GetCommentsSectionParams.encode({
|
||||
ctx: { video_id },
|
||||
@@ -66,6 +173,12 @@ class Proto {
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes comment parameters.
|
||||
*
|
||||
* @param {string} video_id
|
||||
* @returns {string}
|
||||
*/
|
||||
static encodeCommentParams(video_id) {
|
||||
const buf = messages.CreateCommentParams.encode({
|
||||
video_id, params: { index: 0 },
|
||||
@@ -75,6 +188,14 @@ class Proto {
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes comment reply parameters.
|
||||
*
|
||||
* @param {string} comment_id
|
||||
* @param {string} video_id
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
static encodeCommentReplyParams(comment_id, video_id) {
|
||||
const buf = messages.CreateCommentReplyParams.encode({
|
||||
video_id, comment_id,
|
||||
@@ -85,17 +206,51 @@ class Proto {
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
static encodeCommentActionParams(type, comment_id, video_id) {
|
||||
const buf = messages.PeformCommentActionParams.encode({
|
||||
type, comment_id, video_id,
|
||||
unk_num: 2, unk_num_1: 0, unk_num_2: 0,
|
||||
unk_num_3: "0", unk_num_4: 0,
|
||||
unk_num_5: 12, unk_num_6: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* Encodes comment action parameters.
|
||||
*
|
||||
* @param {string} type
|
||||
* @param {string} comment_id
|
||||
* @param {string} video_id
|
||||
* @param {string} [text]
|
||||
* @param {string} [target_language]
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
static encodeCommentActionParams(type, args = {}) {
|
||||
const data = {};
|
||||
|
||||
data.type = type;
|
||||
data.video_id = args.video_id || '';
|
||||
data.comment_id = args.comment_id || '';
|
||||
data.unk_num = 2;
|
||||
|
||||
if (args.hasOwnProperty('text')) {
|
||||
args.comment_id && (delete data.unk_num);
|
||||
|
||||
data.translate_comment_params = {
|
||||
params: {
|
||||
comment: {
|
||||
text: args.text
|
||||
}
|
||||
},
|
||||
comment_id: args.comment_id || '',
|
||||
target_language: args.target_language
|
||||
}
|
||||
}
|
||||
|
||||
const buf = messages.PeformCommentActionParams.encode(data);
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes notification preference parameters.
|
||||
*
|
||||
* @param {string} channel_id
|
||||
* @param {number} index
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
static encodeNotificationPref(channel_id, index) {
|
||||
const buf = messages.NotificationPreferences.encode({
|
||||
channel_id, pref_id: { index },
|
||||
@@ -104,6 +259,29 @@ class Proto {
|
||||
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes sound info parameters.
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {string}
|
||||
*/
|
||||
static encodeSoundInfoParams(id) {
|
||||
const data = {
|
||||
sound: {
|
||||
params: {
|
||||
ids: {
|
||||
id_1: id,
|
||||
id_2: id,
|
||||
id_3: id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const buf = messages.SoundInfoParams.encode(data);
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Proto;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,120 +1,179 @@
|
||||
syntax = "proto2";
|
||||
package proto;
|
||||
|
||||
message NotificationPreferences {
|
||||
required string channel_id = 1;
|
||||
|
||||
message Preference {
|
||||
required int32 index = 1;
|
||||
}
|
||||
Preference pref_id = 2;
|
||||
|
||||
optional int32 number_0 = 3;
|
||||
optional int32 number_1 = 4;
|
||||
}
|
||||
|
||||
message LiveMessageParams {
|
||||
message Params {
|
||||
message Ids {
|
||||
required string channel_id = 1;
|
||||
required string video_id = 2;
|
||||
}
|
||||
Ids ids = 5;
|
||||
}
|
||||
Params params = 1;
|
||||
|
||||
optional int32 number_0 = 2;
|
||||
optional int32 number_1 = 3;
|
||||
}
|
||||
|
||||
message GetCommentsSectionParams {
|
||||
message Context {
|
||||
string video_id = 2;
|
||||
}
|
||||
Context ctx = 2;
|
||||
|
||||
required int32 unk_param = 3;
|
||||
|
||||
message Params {
|
||||
optional string unk_token = 1;
|
||||
|
||||
message Options {
|
||||
required string video_id = 4;
|
||||
required int32 sort_by = 6;
|
||||
required int32 type = 15;
|
||||
}
|
||||
|
||||
message RepliesOptions {
|
||||
required string comment_id = 2;
|
||||
|
||||
message UnkOpts {
|
||||
required int32 unk_param = 1;
|
||||
}
|
||||
UnkOpts unkopts = 4;
|
||||
|
||||
optional string channel_id = 5;
|
||||
required string video_id = 6;
|
||||
|
||||
required int32 unk_param_1 = 8;
|
||||
required int32 unk_param_2 = 9;
|
||||
}
|
||||
|
||||
optional Options opts = 4;
|
||||
optional RepliesOptions replies_opts = 3;
|
||||
|
||||
optional int32 page = 5;
|
||||
required string target = 8;
|
||||
}
|
||||
|
||||
Params params = 6;
|
||||
}
|
||||
|
||||
message CreateCommentParams {
|
||||
required string video_id = 2;
|
||||
message Params {
|
||||
required int32 index = 1;
|
||||
}
|
||||
Params params = 5;
|
||||
required int32 number = 10;
|
||||
}
|
||||
|
||||
message CreateCommentReplyParams {
|
||||
required string video_id = 2;
|
||||
required string comment_id = 4;
|
||||
|
||||
message UnknownParams {
|
||||
required int32 unk_num = 1;
|
||||
}
|
||||
UnknownParams params = 5;
|
||||
|
||||
optional int32 unk_num = 10;
|
||||
}
|
||||
|
||||
message PeformCommentActionParams {
|
||||
required int32 type = 1;
|
||||
optional int32 unk_num = 2;
|
||||
|
||||
required string comment_id = 3;
|
||||
required string video_id = 5;
|
||||
|
||||
optional int32 unk_num_1 = 6;
|
||||
optional int32 unk_num_2 = 7;
|
||||
|
||||
optional string unk_num_3 = 9;
|
||||
|
||||
optional int32 unk_num_4 = 10;
|
||||
optional int32 unk_num_5 = 21;
|
||||
|
||||
optional string channel_id = 23;
|
||||
optional int32 unk_num_6 = 30;
|
||||
}
|
||||
|
||||
message SearchFilter {
|
||||
int32 number = 1;
|
||||
message Filter {
|
||||
int32 param_0 = 1;
|
||||
int32 param_1 = 2;
|
||||
int32 param_2 = 3;
|
||||
}
|
||||
Filter filter = 2;
|
||||
syntax = "proto2";
|
||||
package youtube;
|
||||
|
||||
message VisitorData {
|
||||
string id = 1;
|
||||
int32 timestamp = 5;
|
||||
}
|
||||
|
||||
message ChannelAnalytics {
|
||||
message Params {
|
||||
string channel_id = 1001;
|
||||
}
|
||||
|
||||
Params params = 32;
|
||||
}
|
||||
|
||||
message InnertubePayload {
|
||||
message Context {
|
||||
message Client {
|
||||
int32 unkparam = 16;
|
||||
string client_version = 17;
|
||||
string client_name = 18;
|
||||
}
|
||||
Client client = 1;
|
||||
}
|
||||
|
||||
Context context = 1;
|
||||
|
||||
optional string target = 2;
|
||||
|
||||
message SoundSearchParams {
|
||||
string target_id = 2;
|
||||
string query = 3;
|
||||
}
|
||||
|
||||
optional SoundSearchParams sound_search_params = 16;
|
||||
}
|
||||
|
||||
message SoundInfoParams {
|
||||
message Sound {
|
||||
message Params {
|
||||
message Ids {
|
||||
string id_1 = 1;
|
||||
string id_2 = 2;
|
||||
string id_3 = 3;
|
||||
}
|
||||
Ids ids = 2;
|
||||
}
|
||||
Params params = 1;
|
||||
}
|
||||
|
||||
Sound sound = 94;
|
||||
}
|
||||
|
||||
message NotificationPreferences {
|
||||
string channel_id = 1;
|
||||
|
||||
message Preference {
|
||||
int32 index = 1;
|
||||
}
|
||||
Preference pref_id = 2;
|
||||
|
||||
optional int32 number_0 = 3;
|
||||
optional int32 number_1 = 4;
|
||||
}
|
||||
|
||||
message LiveMessageParams {
|
||||
message Params {
|
||||
message Ids {
|
||||
string channel_id = 1;
|
||||
string video_id = 2;
|
||||
}
|
||||
Ids ids = 5;
|
||||
}
|
||||
Params params = 1;
|
||||
|
||||
optional int32 number_0 = 2;
|
||||
optional int32 number_1 = 3;
|
||||
}
|
||||
|
||||
message GetCommentsSectionParams {
|
||||
message Context {
|
||||
string video_id = 2;
|
||||
}
|
||||
Context ctx = 2;
|
||||
|
||||
int32 unk_param = 3;
|
||||
|
||||
message Params {
|
||||
optional string unk_token = 1;
|
||||
|
||||
message Options {
|
||||
string video_id = 4;
|
||||
int32 sort_by = 6;
|
||||
int32 type = 15;
|
||||
}
|
||||
|
||||
message RepliesOptions {
|
||||
string comment_id = 2;
|
||||
|
||||
message UnkOpts {
|
||||
int32 unk_param = 1;
|
||||
}
|
||||
UnkOpts unkopts = 4;
|
||||
|
||||
optional string channel_id = 5;
|
||||
string video_id = 6;
|
||||
|
||||
int32 unk_param_1 = 8;
|
||||
int32 unk_param_2 = 9;
|
||||
}
|
||||
|
||||
optional Options opts = 4;
|
||||
optional RepliesOptions replies_opts = 3;
|
||||
|
||||
optional int32 page = 5;
|
||||
string target = 8;
|
||||
}
|
||||
|
||||
Params params = 6;
|
||||
}
|
||||
|
||||
message CreateCommentParams {
|
||||
string video_id = 2;
|
||||
message Params {
|
||||
int32 index = 1;
|
||||
}
|
||||
Params params = 5;
|
||||
int32 number = 10;
|
||||
}
|
||||
|
||||
message CreateCommentReplyParams {
|
||||
string video_id = 2;
|
||||
string comment_id = 4;
|
||||
|
||||
message UnknownParams {
|
||||
int32 unk_num = 1;
|
||||
}
|
||||
UnknownParams params = 5;
|
||||
|
||||
optional int32 unk_num = 10;
|
||||
}
|
||||
|
||||
message PeformCommentActionParams {
|
||||
int32 type = 1;
|
||||
string comment_id = 3;
|
||||
string video_id = 5;
|
||||
|
||||
optional int32 unk_num = 2;
|
||||
optional string channel_id = 23;
|
||||
|
||||
message TranslateCommentParams {
|
||||
message Params {
|
||||
message Comment {
|
||||
string text = 1;
|
||||
}
|
||||
Comment comment = 1;
|
||||
}
|
||||
Params params = 3;
|
||||
|
||||
string comment_id = 2;
|
||||
string target_language = 4;
|
||||
}
|
||||
|
||||
optional TranslateCommentParams translate_comment_params = 31;
|
||||
}
|
||||
|
||||
message SearchFilter {
|
||||
optional int32 filter = 1; // almost always sort_by
|
||||
optional int32 no_filter = 19;
|
||||
|
||||
message Filters {
|
||||
optional int32 param_0 = 1;
|
||||
optional int32 param_1 = 2;
|
||||
optional int32 param_2 = 3;
|
||||
}
|
||||
|
||||
optional Filters filters = 2;
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const Utils = require('./Utils');
|
||||
|
||||
module.exports = {
|
||||
URLS: {
|
||||
YT_BASE: 'https://www.youtube.com',
|
||||
@@ -21,59 +19,40 @@ module.exports = {
|
||||
'origin': 'https://www.youtube.com',
|
||||
'user-agent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version',
|
||||
'content-type': 'application/json',
|
||||
'referer': `https://www.youtube.com/tv`,
|
||||
'referer': 'https://www.youtube.com/tv',
|
||||
'accept-language': 'en-US'
|
||||
}
|
||||
},
|
||||
REGEX: {
|
||||
AUTH_SCRIPT: /<script id=\"base-js\" src=\"(.*?)\" nonce=".*?"><\/script>/,
|
||||
CLIENT_IDENTITY: /.+?={};var .+?={clientId:\"(?<id>.+?)\",.+?:\"(?<secret>.+?)\"},/
|
||||
AUTH_SCRIPT: /<script id="base-js" src="(.*?)" nonce=".*?"><\/script>/,
|
||||
CLIENT_IDENTITY: /.+?={};var .+?={clientId:"(?<id>.+?)",.+?:"(?<secret>.+?)"},/
|
||||
}
|
||||
},
|
||||
DEFAULT_HEADERS: (config) => {
|
||||
return {
|
||||
headers: {
|
||||
'Cookie': config?.cookie || '',
|
||||
'user-agent': Utils.getRandomUserAgent('desktop').userAgent,
|
||||
'Referer': 'https://www.google.com/',
|
||||
'Accept': 'text/html',
|
||||
'Accept-Language': `en-${config?.gl || 'US'}`,
|
||||
'Accept-Encoding': 'gzip'
|
||||
}
|
||||
};
|
||||
CLIENTS: {
|
||||
WEB: {
|
||||
NAME: 'WEB'
|
||||
},
|
||||
YTMUSIC: {
|
||||
NAME: 'WEB_REMIX',
|
||||
VERSION: '1.20211213.00.00'
|
||||
},
|
||||
ANDROID: {
|
||||
NAME: 'ANDROID',
|
||||
VERSION: '17.17.32'
|
||||
}
|
||||
},
|
||||
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',
|
||||
},
|
||||
VIDEO_INFO_REQBODY: (id, sts, context) => {
|
||||
return {
|
||||
playbackContext: {
|
||||
contentPlaybackContext: {
|
||||
'currentUrl': '/watch?v=' + id,
|
||||
'vis': 0,
|
||||
'splay': false,
|
||||
'autoCaptionsDefaultOn': false,
|
||||
'autonavState': 'STATE_OFF',
|
||||
'html5Preference': 'HTML5_PREF_WANTS',
|
||||
'signatureTimestamp': sts,
|
||||
'referer': 'https://www.youtube.com',
|
||||
'lactMilliseconds': '-1'
|
||||
}
|
||||
},
|
||||
context: context,
|
||||
videoId: id
|
||||
};
|
||||
},
|
||||
YTMUSIC_VERSION: '1.20211213.00.00',
|
||||
METADATA_KEYS: [
|
||||
'embed', 'view_count', 'average_rating', 'allow_ratings',
|
||||
'length_seconds', 'channel_id', 'channel_url',
|
||||
@@ -104,11 +83,15 @@ module.exports = {
|
||||
NORMAL: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'.split(''),
|
||||
REVERSE: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'.split('')
|
||||
},
|
||||
SIG_REGEX: {
|
||||
ACTIONS: /;.{2}\.(?<name>.{2})\(.*?,(?<param>.*?)\)/g,
|
||||
FUNCTIONS: /(?<name>.{2}):function\(.*?\){(.*?)}/g
|
||||
},
|
||||
NTOKEN_REGEX: {
|
||||
CALLS: /c\[(.*?)\]\((.+?)\)/g,
|
||||
PLACEHOLDERS: /c\[(.*?)\]=c/g,
|
||||
FUNCTIONS: /d\.push\(e\)|d\.reverse\(\)|d\[0\]\)\[0\]\)|f=d\[0];d\[0\]|d\.length;d\.splice\(e,1\)|function\(\){for\(var|function\(d,e,f\){var|function\(d\){for\(var|reverse\(\)\.forEach|unshift\(d\.pop\(\)\)|function\(d,e\){for\(var f/
|
||||
},
|
||||
FUNCS_REGEX: /d\.push\(e\)|d\.reverse\(\)|d\[0\]\)\[0\]\)|f=d\[0];d\[0\]|d\.length;d\.splice\(e,1\)|function\(\){for\(var|function\(d,e,f\){var|function\(d\){for\(var|reverse\(\)\.forEach|unshift\(d\.pop\(\)\)|function\(d,e\){for\(var f/,
|
||||
FUNCS: {
|
||||
PUSH: 'd.push(e)',
|
||||
REVERSE_1: 'd.reverse()',
|
||||
@@ -122,4 +105,4 @@ module.exports = {
|
||||
TRANSLATE_1: 'function(d,e){for(var f',
|
||||
TRANSLATE_2: 'function(d,e,f){var'
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -4,50 +4,119 @@ const Axios = require('axios');
|
||||
const Utils = require('./Utils');
|
||||
const Constants = require('./Constants');
|
||||
|
||||
/** @namespace */
|
||||
class Request {
|
||||
constructor (session) {
|
||||
/**
|
||||
* @param {Innertube} session
|
||||
* @constructor
|
||||
*/
|
||||
constructor(session) {
|
||||
this.session = session;
|
||||
|
||||
|
||||
this.instance = Axios.create({
|
||||
baseURL: Constants.URLS.YT_BASE_API + session.version,
|
||||
headers: Constants.INNERTUBE_HEADERS_BASE,
|
||||
params: { key: session.key },
|
||||
params: { key: session.key, prettyPrint: false },
|
||||
validateStatus: () => true,
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
|
||||
this.#setupInterceptor();
|
||||
|
||||
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
|
||||
#setupInterceptor() {
|
||||
this.instance.interceptors.request.use((config) => {
|
||||
const is_ytmusic = config.data.includes(Constants.URLS.YT_MUSIC);
|
||||
const is_json_payload = typeof config.data == 'object';
|
||||
|
||||
config.headers['user-agent'] = Utils.getRandomUserAgent('desktop').userAgent;
|
||||
config.headers['accept-language'] = `en-${this.session.config.gl || 'US'}`;
|
||||
config.headers['x-goog-visitor-id'] = this.session.context.client.visitorData || ''
|
||||
config.headers['x-goog-visitor-id'] = this.session.context.client.visitorData || '';
|
||||
config.headers['x-youtube-client-version'] = this.session.context.client.clientVersion;
|
||||
config.headers['x-origin'] = is_ytmusic && Constants.URLS.YT_MUSIC || Constants.URLS.YT_BASE;
|
||||
config.headers['origin'] = is_ytmusic && Constants.URLS.YT_MUSIC || Constants.URLS.YT_BASE;
|
||||
|
||||
is_ytmusic && (config.baseURL = Constants.URLS.YT_MUSIC_BASE_API + this.session.version);
|
||||
|
||||
if (is_json_payload) {
|
||||
config.data = {
|
||||
context: JSON.parse(JSON.stringify(this.session.context)), // deep copies the context object
|
||||
...config.data
|
||||
};
|
||||
|
||||
this.#adjustContext(config.data.context, config.data.client);
|
||||
|
||||
config.headers['x-youtube-client-version'] = config.data.context.client.clientVersion;
|
||||
config.headers['x-origin'] = config.data.context.client.originalUrl;
|
||||
config.headers['origin'] = config.data.context.client.originalUrl;
|
||||
|
||||
config.data.client == 'YTMUSIC' &&
|
||||
(config.baseURL = Constants.URLS.YT_MUSIC_BASE_API + this.session.version);
|
||||
|
||||
delete config.data.client;
|
||||
}
|
||||
|
||||
if (this.session.logged_in) {
|
||||
const cookie = this.session.config.cookie;
|
||||
|
||||
const token = cookie
|
||||
&& this.session.auth_apisid
|
||||
|| this.session.access_token;
|
||||
|
||||
const token = cookie &&
|
||||
this.session.auth_apisid ||
|
||||
this.session.access_token;
|
||||
|
||||
config.headers.cookie = cookie || '';
|
||||
config.headers.authorization = cookie && token || `Bearer ${token}`;
|
||||
|
||||
!cookie && (delete config.params.key);
|
||||
}
|
||||
|
||||
this.session.config.debug &&
|
||||
console.info('\n', '[' + config.method.toUpperCase() + ']', '>', config.baseURL + config.url, '\n', config?.data, '\n');
|
||||
|
||||
return config;
|
||||
}, (error) => Promise.reject(error));
|
||||
}, (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);
|
||||
|
||||
return response;
|
||||
});
|
||||
|
||||
this.instance.interceptors.response.use(undefined, (error) => {
|
||||
if (error.info) return Promise.reject(error);
|
||||
throw new Utils.InnertubeError('Could not complete this operation', error.message);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the context according to the given client.
|
||||
* @todo refactor this?
|
||||
* @returns
|
||||
*/
|
||||
#adjustContext(ctx, client) {
|
||||
switch (client) {
|
||||
case 'YTMUSIC':
|
||||
ctx.client.originalUrl = Constants.URLS.YT_MUSIC;
|
||||
ctx.client.clientVersion = Constants.CLIENTS.YTMUSIC.VERSION;
|
||||
ctx.client.clientName = Constants.CLIENTS.YTMUSIC.NAME;
|
||||
break;
|
||||
case 'ANDROID':
|
||||
ctx.client.originalUrl = Constants.URLS.YT_BASE;
|
||||
ctx.client.clientVersion = Constants.CLIENTS.ANDROID.VERSION;
|
||||
ctx.client.clientName = Constants.CLIENTS.ANDROID.NAME;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Request;
|
||||
module.exports = Request;
|
||||
@@ -4,42 +4,54 @@ const Crypto = require('crypto');
|
||||
const UserAgent = require('user-agents');
|
||||
const Flatten = require('flat');
|
||||
|
||||
function InnertubeError(message, info) {
|
||||
this.info = info || {};
|
||||
this.stack = Error(message).stack;
|
||||
/** @namespace */
|
||||
class InnertubeError extends Error {
|
||||
constructor (message, info) {
|
||||
super(message);
|
||||
|
||||
info && (this.info = info);
|
||||
|
||||
this.date = new Date();
|
||||
this.version = require('../../package.json').version;
|
||||
}
|
||||
}
|
||||
|
||||
InnertubeError.prototype = Object.create(Error.prototype);
|
||||
InnertubeError.prototype.constructor = InnertubeError;
|
||||
|
||||
class ParsingError extends InnertubeError {};
|
||||
class DownloadError extends InnertubeError {};
|
||||
class MissingParamError extends InnertubeError {};
|
||||
class UnavailableContentError extends InnertubeError {};
|
||||
class NoStreamingDataError extends InnertubeError {};
|
||||
class ParsingError extends InnertubeError {}
|
||||
class DownloadError extends InnertubeError {}
|
||||
class MissingParamError extends InnertubeError {}
|
||||
class UnavailableContentError extends InnertubeError {}
|
||||
class NoStreamingDataError extends InnertubeError {}
|
||||
|
||||
/**
|
||||
* Utility to help access deep properties of an object.
|
||||
*
|
||||
* @param {object} obj - The object.
|
||||
* @param {string} key - Key of the property being accessed.
|
||||
* @param {string} target - Anything that might be inside of the property.
|
||||
* @param {number} depth - Maximum number of nested objects to flatten.
|
||||
* @param {boolean} safe - If set to true arrays will be preserved.
|
||||
* @param {object} obj - the object.
|
||||
* @param {string} key - key of the property being accessed.
|
||||
* @param {string} target - anything that might be inside of the property.
|
||||
* @param {number} depth - maximum number of nested objects to flatten.
|
||||
* @param {boolean} safe - if set to true arrays will be preserved.
|
||||
*
|
||||
* @returns {object|Array.<any>}
|
||||
*/
|
||||
function findNode(obj, key, target, depth, safe = true) {
|
||||
const flat_obj = Flatten(obj, { safe, maxDepth: depth || 2 });
|
||||
const result = Object.keys(flat_obj).find((entry) => entry.includes(key) && JSON.stringify(flat_obj[entry] || '{}').includes(target));
|
||||
if (!result) throw new ParsingError(`Expected to find "${key}" with content "${target}" but got ${result}`, { key, target, data_snippet: `${JSON.stringify(flat_obj).slice(0, 300)}..` });
|
||||
if (!result) throw new ParsingError(`Expected to find "${key}" with content "${target}" but got ${result}`, { key, target, data_snippet: `${JSON.stringify(flat_obj, null, 4).slice(0, 300)}..` });
|
||||
return flat_obj[result];
|
||||
}
|
||||
|
||||
function escapeStringRegexp(string) {
|
||||
return string.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a string between two delimiters.
|
||||
* Finds a string between two delimiters.
|
||||
*
|
||||
* @param {string} data - The data.
|
||||
* @param {string} start_string - Start string.
|
||||
* @param {string} end_string - End string.
|
||||
* @param {string} data - the data.
|
||||
* @param {string} start_string - start string.
|
||||
* @param {string} end_string - end string.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
function getStringBetweenStrings(data, start_string, end_string) {
|
||||
const regex = new RegExp(`${escapeStringRegexp(start_string)}(.*?)${escapeStringRegexp(end_string)}`, 's');
|
||||
@@ -47,10 +59,6 @@ function getStringBetweenStrings(data, start_string, end_string) {
|
||||
return match ? match[1] : undefined;
|
||||
}
|
||||
|
||||
function escapeStringRegexp(string) {
|
||||
return string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random user agent.
|
||||
*
|
||||
@@ -85,6 +93,23 @@ function generateSidAuth(sid) {
|
||||
return ['SAPISIDHASH', [timestamp, gen_hash].join('_')].join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random string with a given length.
|
||||
*
|
||||
* @param {number} length
|
||||
* @returns {string}
|
||||
*/
|
||||
function generateRandomString(length) {
|
||||
const result = [];
|
||||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
result.push(alphabet.charAt(Math.floor(Math.random() * alphabet.length)));
|
||||
}
|
||||
|
||||
return result.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts time (h:m:s) to seconds.
|
||||
*
|
||||
@@ -110,6 +135,29 @@ function camelToSnake(string) {
|
||||
return string[0].toLowerCase() + string.slice(1, string.length).replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given client is valid.
|
||||
*
|
||||
* @param {string} client
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isValidClient(client) {
|
||||
return [ 'YOUTUBE', 'YTMUSIC' ].includes(client);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an error if given parameters are undefined.
|
||||
*
|
||||
* @param {object} params
|
||||
* @returns
|
||||
*/
|
||||
function throwIfMissing(params) {
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (!value)
|
||||
throw new MissingParamError(`${key} is missing`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns the ntoken transform data into a valid json array
|
||||
*
|
||||
@@ -126,7 +174,7 @@ function refineNTokenData(data) {
|
||||
.replace(/""/g, '').replace(/length]\)}"/g, 'length])}');
|
||||
}
|
||||
|
||||
const errors = { UnavailableContentError, ParsingError, DownloadError, InnertubeError, MissingParamError, NoStreamingDataError };
|
||||
const functions = { findNode, getRandomUserAgent, generateSidAuth, getStringBetweenStrings, camelToSnake, timeToSeconds, refineNTokenData };
|
||||
const errors = { InnertubeError, UnavailableContentError, ParsingError, DownloadError, MissingParamError, NoStreamingDataError };
|
||||
const functions = { findNode, getRandomUserAgent, generateSidAuth, generateRandomString, getStringBetweenStrings, camelToSnake, isValidClient, throwIfMissing, timeToSeconds, refineNTokenData };
|
||||
|
||||
module.exports = { ...functions, ...errors };
|
||||
6982
package-lock.json
generated
6982
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "1.4.2",
|
||||
"description": "A full-featured library that allows you to get detailed info about any video, subscribe, unsubscribe, like, dislike, comment, search, download videos/music and much more!",
|
||||
"version": "1.4.3",
|
||||
"description": "A full-featured wrapper around YouTube's private API. Allows you to retrieve info about any video, subscribe, unsubscribe, like, dislike, comment, search, download videos/music and much more!",
|
||||
"main": "index.js",
|
||||
"author": "LuanRT <luan.lrt4@gmail.com> (https://github.com/LuanRT)",
|
||||
"funding": "https://ko-fi.com/luanrt",
|
||||
@@ -10,7 +10,10 @@
|
||||
"node": ">=14"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node test"
|
||||
"test": "jest",
|
||||
"lint": "eslint ./",
|
||||
"lint:fix": "eslint --fix ./",
|
||||
"build:types": "npx tsc"
|
||||
},
|
||||
"types": "./typings/index.d.ts",
|
||||
"directories": {
|
||||
@@ -26,9 +29,15 @@
|
||||
"user-agents": "^1.0.778",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^17.0.31",
|
||||
"typescript": "^4.6.4",
|
||||
"eslint": "^8.15.0",
|
||||
"jest": "^28.1.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https//github.com/LuanRT/YouTube.js.git"
|
||||
"url": "git+https://github.com/LuanRT/YouTube.js.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/LuanRT/YouTube.js/issues"
|
||||
@@ -36,21 +45,23 @@
|
||||
"homepage": "https://github.com/LuanRT/YouTube.js#readme",
|
||||
"keywords": [
|
||||
"yt",
|
||||
"dl",
|
||||
"ytdl",
|
||||
"youtube",
|
||||
"youtube-dl",
|
||||
"youtubedl",
|
||||
"youtube-dl",
|
||||
"youtube-downloader",
|
||||
"innertube",
|
||||
"innertubeapi",
|
||||
"unofficial",
|
||||
"downloader",
|
||||
"livechat",
|
||||
"ytmusic",
|
||||
"dislike",
|
||||
"search",
|
||||
"comment",
|
||||
"music",
|
||||
"like",
|
||||
"api",
|
||||
"dl"
|
||||
"api"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
test_url: 's=t%3DQ%3DAv2TLJ2sbQFV5msp4j7v71gS1rsXNd6QH2V1KpxGlaOD%3DIC46mVzTVTW_2zttE32HKH7XO1jkyfOJs58avqMLKdvRdgIQRw8JQ0qOA&sp=sig&url=https://r1---sn-hxtxgcg-8qjl.googlevideo.com/videoplayback%3Fexpire%3D1635863482%26ei%3DWveAYdqsB6KPobIPjtWwYA%26ip%3D128.201.98.50%26id%3Do-ABuHwkfRnd4hOQoDKRKn7ZHvuLEPAPKkYhiYKpTwLrY7%26itag%3D18%26source%3Dyoutube%26requiressl%3Dyes%26mh%3DG3%26mm%3D31%252C29%26mn%3Dsn-hxtxgcg-8qjl%252Csn-gpv7dned%26ms%3Dau%252Crdu%26mv%3Dm%26mvi%3D1%26pl%3D24%26initcwndbps%3D397500%26vprv%3D1%26mime%3Dvideo%252Fmp4%26ns%3Dv9CYauI2ycUgrV6wOERCNxsG%26gir%3Dyes%26clen%3D7275579%26ratebypass%3Dyes%26dur%3D218.290%26lmt%3D1540416860737282%26mt%3D1635841731%26fvip%3D4%26fexp%3D24001373%252C24007246%26c%3DWEB%26txp%3D5531432%26n%3DD8yGa-DC5m2Dwv--%26sparams%3Dexpire%252Cei%252Cip%252Cid%252Citag%252Csource%252Crequiressl%252Cvprv%252Cmime%252Cns%252Cgir%252Cclen%252Cratebypass%252Cdur%252Clmt%26lsparams%3Dmh%252Cmm%252Cmn%252Cms%252Cmv%252Cmvi%252Cpl%252Cinitcwndbps%26lsig%3DAG3C_xAwRAIgdS6ux5rh5ulfwh8c6_Kt2cOdyS51OxPlxSUoB5k5x9YCICOgRiuFsZwAqJmxvBrCuq3CKk1S4YeAxEq3zPLvzAvX',
|
||||
expected_url: 'https://r1---sn-hxtxgcg-8qjl.googlevideo.com/videoplayback?expire=1635863482&ei=WveAYdqsB6KPobIPjtWwYA&ip=128.201.98.50&id=o-ABuHwkfRnd4hOQoDKRKn7ZHvuLEPAPKkYhiYKpTwLrY7&itag=18&source=youtube&requiressl=yes&mh=G3&mm=31%2C29&mn=sn-hxtxgcg-8qjl%2Csn-gpv7dned&ms=au%2Crdu&mv=m&mvi=1&pl=24&initcwndbps=397500&vprv=1&mime=video%2Fmp4&ns=v9CYauI2ycUgrV6wOERCNxsG&gir=yes&clen=7275579&ratebypass=yes&dur=218.290&lmt=1540416860737282&mt=1635841731&fvip=4&fexp=24001373%2C24007246&c=WEB&txp=5531432&n=D8yGa-DC5m2Dwv--&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRAIgdS6ux5rh5ulfwh8c6_Kt2cOdyS51OxPlxSUoB5k5x9YCICOgRiuFsZwAqJmxvBrCuq3CKk1S4YeAxEq3zPLvzAvX&sig=AOq0QJ8wRQIgdRvdKLMqva85sJOfykj1OX7HKH23Ettz2_WTVTzVm64CIQDOalGxpK1V2Ht6dNXsr1Sg17v7j4psm5VFQbs2JLT2vA%3D%3D',
|
||||
original_ntoken: 'PqjqqJjdB9K821VIisj',
|
||||
expected_ntoken: 'AxwyS-osUl1WhMUd1',
|
||||
client_version: '2.20211101.01.00',
|
||||
test_video_id: 'dQw4w9WgXcQ',
|
||||
test_song_id: 'UGB_Bsm5Unk',
|
||||
sig_decipher_sc: `fB={RP:function(a,b){a.splice(0,b)},
|
||||
VIDEOS: [
|
||||
{
|
||||
ID: 'bUHZ2k9DYHY',
|
||||
QUERY: 'Space DOES NOT expand everywhere'
|
||||
},
|
||||
{
|
||||
ID: 'WSeNSzJ2-Jw',
|
||||
QUERY: 'Scary Monsters and Nice Sprites Official Audio'
|
||||
}
|
||||
],
|
||||
DECIPHERS: {
|
||||
SIG: {
|
||||
ORIGINAL_URL: 's=t%3DQ%3DAv2TLJ2sbQFV5msp4j7v71gS1rsXNd6QH2V1KpxGlaOD%3DIC46mVzTVTW_2zttE32HKH7XO1jkyfOJs58avqMLKdvRdgIQRw8JQ0qOA&sp=sig&url=https://r1---sn-hxtxgcg-8qjl.googlevideo.com/videoplayback%3Fexpire%3D1635863482%26ei%3DWveAYdqsB6KPobIPjtWwYA%26ip%3D128.201.98.50%26id%3Do-ABuHwkfRnd4hOQoDKRKn7ZHvuLEPAPKkYhiYKpTwLrY7%26itag%3D18%26source%3Dyoutube%26requiressl%3Dyes%26mh%3DG3%26mm%3D31%252C29%26mn%3Dsn-hxtxgcg-8qjl%252Csn-gpv7dned%26ms%3Dau%252Crdu%26mv%3Dm%26mvi%3D1%26pl%3D24%26initcwndbps%3D397500%26vprv%3D1%26mime%3Dvideo%252Fmp4%26ns%3Dv9CYauI2ycUgrV6wOERCNxsG%26gir%3Dyes%26clen%3D7275579%26ratebypass%3Dyes%26dur%3D218.290%26lmt%3D1540416860737282%26mt%3D1635841731%26fvip%3D4%26fexp%3D24001373%252C24007246%26c%3DWEB%26txp%3D5531432%26n%3DD8yGa-DC5m2Dwv--%26sparams%3Dexpire%252Cei%252Cip%252Cid%252Citag%252Csource%252Crequiressl%252Cvprv%252Cmime%252Cns%252Cgir%252Cclen%252Cratebypass%252Cdur%252Clmt%26lsparams%3Dmh%252Cmm%252Cmn%252Cms%252Cmv%252Cmvi%252Cpl%252Cinitcwndbps%26lsig%3DAG3C_xAwRAIgdS6ux5rh5ulfwh8c6_Kt2cOdyS51OxPlxSUoB5k5x9YCICOgRiuFsZwAqJmxvBrCuq3CKk1S4YeAxEq3zPLvzAvX',
|
||||
DECIPHERED_URL: 'https://r1---sn-hxtxgcg-8qjl.googlevideo.com/videoplayback?expire=1635863482&ei=WveAYdqsB6KPobIPjtWwYA&ip=128.201.98.50&id=o-ABuHwkfRnd4hOQoDKRKn7ZHvuLEPAPKkYhiYKpTwLrY7&itag=18&source=youtube&requiressl=yes&mh=G3&mm=31%2C29&mn=sn-hxtxgcg-8qjl%2Csn-gpv7dned&ms=au%2Crdu&mv=m&mvi=1&pl=24&initcwndbps=397500&vprv=1&mime=video%2Fmp4&ns=v9CYauI2ycUgrV6wOERCNxsG&gir=yes&clen=7275579&ratebypass=yes&dur=218.290&lmt=1540416860737282&mt=1635841731&fvip=4&fexp=24001373%2C24007246&c=WEB&txp=5531432&n=D8yGa-DC5m2Dwv--&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRAIgdS6ux5rh5ulfwh8c6_Kt2cOdyS51OxPlxSUoB5k5x9YCICOgRiuFsZwAqJmxvBrCuq3CKk1S4YeAxEq3zPLvzAvX&sig=AOq0QJ8wRQIgdRvdKLMqva85sJOfykj1OX7HKH23Ettz2_WTVTzVm64CIQDOalGxpK1V2Ht6dNXsr1Sg17v7j4psm5VFQbs2JLT2vA%3D%3D',
|
||||
ALGORITHM: `fB={RP:function(a,b){a.splice(0,b)},
|
||||
Td:function(a){a.reverse()},
|
||||
kq:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c};fB.kq(a,35);fB.RP(a,2);fB.kq(a,46);fB.Td(a,6);`,
|
||||
n_scramble_sc: `var b=a.split(""),c=[-470482026,-691770757,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(-e).reverse().forEach(function(f){d.unshift(f)})},
|
||||
kq:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c};fB.kq(a,35);fB.RP(a,2);fB.kq(a,46);fB.Td(a,6);`
|
||||
},
|
||||
N: {
|
||||
ORIGINAL_TOKEN: 'PqjqqJjdB9K821VIisj',
|
||||
DECIPHERED_TOKEN: 'AxwyS-osUl1WhMUd1',
|
||||
ALGORITHM: `var b=a.split(""),c=[-470482026,-691770757,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(-e).reverse().forEach(function(f){d.unshift(f)})},
|
||||
b,258876269,-1426380890,318754300,-68090711,-2064438462,-1886316521,1913911047,1635047330,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(-e).reverse().forEach(function(f){d.unshift(f)})},
|
||||
-1815897225,1940621629,-714586149,-1723898467,null,778601498,2145333248,1245726977,1952270083,268207944,244274044,null,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(0,1,d.splice(e,1,d[0])[0])},
|
||||
null,-762271981,604636391,1087224318,-931565987,-338396815,function(d,e){for(e=(e%d.length+d.length)%d.length;e--;)d.unshift(d.pop())},
|
||||
@@ -22,4 +31,6 @@ null,-762271981,604636391,1087224318,-931565987,-338396815,function(d,e){for(e=(
|
||||
"push",function(d,e){for(var f=64,h=[];++f-h.length-32;)switch(f){case 58:f=96;continue;case 91:f=44;break;case 65:f=47;continue;case 46:f=153;case 123:f-=58;default:h.push(String.fromCharCode(f))}d.forEach(function(l,m,n){this.push(n[m]=h[(h.indexOf(l)-h.indexOf(this[m])+m-32+f--)%h.length])},e.split(""))}];
|
||||
c[17]=c;c[24]=c;c[26]=c;try{c[45](c[17],c[38]),c[12](c[44],c[29]),c[45](c[26],c[0]),c[51](c[41],c[13]),c[12](c[41],c[27]),c[12](c[26],c[11]),c[39](c[17],c[49]),c[9](c[38],c[47]),c[26](c[40],c[0]),c[7](c[8],c[44]),c[14](c[54],c[0]),c[18](c[3],c[25]),c[7](c[33],c[36]),c[15](c[19],c[14]),c[7](c[19],c[9]),c[7](c[6],c[12]),c[41](c[33],c[35]),c[7](c[40],c[5]),c[50](c[42]),c[13](c[14],c[17]),c[6](c[35],c[51]),c[26](c[48],c[50]),c[26](c[35],c[0]),c[6](c[21],c[46]),c[15](c[21],c[42]),c[1](c[2],c[43]),c[15](c[2],
|
||||
c[31]),c[1](c[21],c[25]),c[22](c[30],c[17]),c[15](c[44],c[46]),c[22](c[44],c[11]),c[22](c[23],c[38]),c[1](c[23],c[14]),c[35](c[23],c[44]),c[11](c[53],c[20]),c[9](c[51]),c[31](c[51],c[28]),c[18](c[51],c[35]),c[46](c[53],c[6]),c[52](c[51],c[49]),c[11](c[53],c[15])}catch(d){return"enhanced_except_75MBkOz-_w8_"+a} return b.join("");`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const Fs = require('fs');
|
||||
const Innertube = require('..');
|
||||
const NToken = require('../lib/deciphers/NToken');
|
||||
const Signature = require('../lib/deciphers/Signature');
|
||||
const Constants = require('./constants');
|
||||
|
||||
let failed_tests_count = 0;
|
||||
|
||||
async function performTests() {
|
||||
const youtube = await new Innertube().catch((error) => error);
|
||||
assert(!(youtube instanceof Error), `should retrieve Innertube configuration data`, youtube);
|
||||
|
||||
if (!(youtube instanceof Error)) {
|
||||
const homefeed = await youtube.getHomeFeed().catch((error) => error);
|
||||
assert(!(homefeed instanceof Error), `should retrieve recommendations`, homefeed);
|
||||
|
||||
const ytsearch = await youtube.search('Carl Sagan - Documentary').catch((error) => error);
|
||||
assert(!(ytsearch instanceof Error), `should search on YouTube`, ytsearch);
|
||||
|
||||
const ytmsearch = await youtube.search('Logic - Obediently Yours', { client: 'YTMUSIC' }).catch((error) => error);
|
||||
assert(!(ytmsearch instanceof Error), `should search on YouTube Music`, ytmsearch);
|
||||
|
||||
const ytsearch_suggestions = await youtube.getSearchSuggestions('test', { client: 'YOUTUBE' }).catch((error) => error);
|
||||
assert(!(ytsearch_suggestions instanceof Error), `should retrieve YouTube search suggestions`, ytsearch_suggestions);
|
||||
|
||||
const ytmsearch_suggestions = await youtube.getSearchSuggestions('test', { client: 'YTMUSIC' }).catch((error) => error);
|
||||
assert(!(ytmsearch_suggestions instanceof Error), `should retrieve YouTube Music search suggestions`, ytmsearch_suggestions);
|
||||
|
||||
const details = await youtube.getDetails(Constants.test_video_id).catch((error) => error);
|
||||
assert(!(details instanceof Error), `should retrieve details for ${Constants.test_video_id}`, details);
|
||||
|
||||
const comments = await youtube.getComments(Constants.test_video_id).catch((error) => error);
|
||||
assert(!(comments instanceof Error), `should retrieve comments for ${Constants.test_video_id}`, comments);
|
||||
|
||||
const ytplaylist = await youtube.getPlaylist(ytmsearch?.results?.community_playlists[0].id, { client: 'YOUTUBE' }).catch((error) => error);
|
||||
assert(!(ytplaylist instanceof Error), `should retrieve and parse playlist with YouTube`, ytplaylist);
|
||||
|
||||
const ytmplaylist = await youtube.getPlaylist(ytmsearch?.results?.community_playlists[0].id, { client: 'YTMUSIC' }).catch((error) => error);
|
||||
assert(!(ytmplaylist instanceof Error), `should retrieve and parse playlist with YouTube Music`, ytmplaylist);
|
||||
|
||||
const lyrics = await youtube.getLyrics(Constants.test_song_id).catch((error) => error);
|
||||
assert(!(lyrics instanceof Error), `should retrieve song lyrics`, lyrics);
|
||||
|
||||
const video = await downloadVideo(Constants.test_video_id, youtube).catch((error) => error);
|
||||
assert(!(video instanceof Error), `should download video (${Constants.test_video_id})`, video);
|
||||
}
|
||||
|
||||
const n_token = new NToken(Constants.n_scramble_sc, Constants.original_ntoken).transform();
|
||||
assert(n_token == Constants.expected_ntoken, `should transform n token into ${Constants.expected_ntoken}`, n_token);
|
||||
|
||||
const transformed_url = new Signature(Constants.test_url, { sig_decipher_sc: Constants.sig_decipher_sc }).decipher();
|
||||
assert(transformed_url == Constants.expected_url, `should decipher signature`, transformed_url);
|
||||
|
||||
if (failed_tests_count > 0)
|
||||
throw new Error(`${failed_tests_count} tests have failed`);
|
||||
}
|
||||
|
||||
function downloadVideo(id, youtube) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let got_video_info = false;
|
||||
const stream = youtube.download(id, { type: 'videoandaudio' });
|
||||
stream.pipe(Fs.createWriteStream(`./${id}.mp4`));
|
||||
stream.on('end', () => Fs.existsSync(`./${id}.mp4`) && got_video_info && resolve() || reject());
|
||||
stream.on('info', () => got_video_info = true);
|
||||
stream.on('error', (err) => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
function assert(outcome, description, data) {
|
||||
const pass_fail = outcome ? 'pass' : 'fail';
|
||||
|
||||
console.info(pass_fail, ':', description);
|
||||
!outcome && (failed_tests_count += 1);
|
||||
!outcome && console.error('Error: ', data);
|
||||
|
||||
return outcome;
|
||||
}
|
||||
|
||||
performTests();
|
||||
111
test/main.test.js
Normal file
111
test/main.test.js
Normal file
@@ -0,0 +1,111 @@
|
||||
'use strict';
|
||||
|
||||
const Fs = require('fs');
|
||||
const Innertube = require('..');
|
||||
const NToken = require('../lib/deciphers/NToken');
|
||||
const Signature = require('../lib/deciphers/Signature');
|
||||
const Constants = require('./constants');
|
||||
|
||||
describe('YouTube.js Tests', () => {
|
||||
beforeAll(async () => {
|
||||
this.session = await new Innertube();
|
||||
});
|
||||
|
||||
describe('Search', () => {
|
||||
it('Should search on YouTube', async () => {
|
||||
const search = await this.session.search(Constants.VIDEOS[0].QUERY, { client: 'YOUTUBE' });
|
||||
expect(search.videos.length).toBeLessThanOrEqual(20);
|
||||
});
|
||||
|
||||
it('Should search on YouTube Music', async () => {
|
||||
const search = await this.session.search(Constants.VIDEOS[1].QUERY, { client: 'YTMUSIC' });
|
||||
expect(search.results.songs.length).toBeLessThanOrEqual(3);
|
||||
});
|
||||
|
||||
it('Should retrieve YouTube search suggestions', async () => {
|
||||
const suggestions = await this.session.getSearchSuggestions(Constants.VIDEOS[0].QUERY, { client: 'YOUTUBE' });
|
||||
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.results.length).toBeLessThanOrEqual(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Comments', () => {
|
||||
it('Should retrieve comments', async () => {
|
||||
this.comments = await this.session.getComments(Constants.VIDEOS[1].ID);
|
||||
expect(this.comments.items.length).toBeLessThanOrEqual(20);
|
||||
});
|
||||
|
||||
it('Should retrieve comment thread continuation', async () => {
|
||||
const next = await this.comments.getContinuation();
|
||||
expect(next.items.length).toBeLessThanOrEqual(20);
|
||||
});
|
||||
|
||||
it('Should retrieve comment replies', async () => {
|
||||
const top_comment = this.comments.items[0];
|
||||
const replies = await top_comment.getReplies();
|
||||
expect(replies.items.length).toBeLessThanOrEqual(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Playlists', () => {
|
||||
it('Should retrieve playlist with YouTube', async () => {
|
||||
const playlist = await this.session.getPlaylist('PLLw0AzOz95FU7w2juhPECP9NyGhbZmz_t', { client: 'YOUTUBE' });
|
||||
expect(playlist.items.length).toBeLessThanOrEqual(100);
|
||||
});
|
||||
|
||||
it('Should retrieve playlist with YouTube Music', async () => {
|
||||
const playlist = await this.session.getPlaylist('PLLw0AzOz95FU7w2juhPECP9NyGhbZmz_t', { client: 'YTMUSIC' });
|
||||
expect(playlist.items.length).toBeLessThanOrEqual(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('General', () => {
|
||||
it('Should retrieve home feed', async () => {
|
||||
const homefeed = await this.session.getHomeFeed();
|
||||
expect(homefeed.videos.length).toBeLessThanOrEqual(30);
|
||||
});
|
||||
|
||||
it('Should retrieve trending content', async () => {
|
||||
const trending = await this.session.getTrending();
|
||||
expect(trending.now.content[0].videos.length).toBeLessThanOrEqual(100);
|
||||
});
|
||||
|
||||
it('Should retrieve video info', async () => {
|
||||
const details = await this.session.getDetails(Constants.VIDEOS[0].ID);
|
||||
expect(details.id).toBe(Constants.VIDEOS[0].ID);
|
||||
});
|
||||
|
||||
it('Should download video', async () => {
|
||||
const result = await download(Constants.VIDEOS[1].ID, this.session);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Deciphers', () => {
|
||||
it('Should decipher signature', () => {
|
||||
const result = new Signature(Constants.DECIPHERS.SIG.ORIGINAL_URL, Constants.DECIPHERS.SIG.ALGORITHM).decipher();
|
||||
expect(result).toEqual(Constants.DECIPHERS.SIG.DECIPHERED_URL);
|
||||
});
|
||||
|
||||
it('Should decipher ntoken', () => {
|
||||
const result = new NToken(Constants.DECIPHERS.N.ALGORITHM, Constants.DECIPHERS.N.ORIGINAL_TOKEN).transform();
|
||||
expect(result).toEqual(Constants.DECIPHERS.N.DECIPHERED_TOKEN);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function download(id, session) {
|
||||
let got_video_info = false;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const stream = session.download(id, { type: 'videoandaudio' });
|
||||
stream.pipe(Fs.createWriteStream(`./${id}.mp4`));
|
||||
stream.on('end', () => resolve(Fs.existsSync(`./${id}.mp4`) && got_video_info));
|
||||
stream.on('info', () => got_video_info = true);
|
||||
stream.on('error', () => resolve(false));
|
||||
});
|
||||
}
|
||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"allowJs": true,
|
||||
"outDir": "./typings",
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": [
|
||||
"./lib/**/*.js",
|
||||
"./index.js"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
||||
192
typings/index.d.ts
vendored
192
typings/index.d.ts
vendored
@@ -1,190 +1,2 @@
|
||||
interface AuthInfo {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
expires: Date;
|
||||
}
|
||||
|
||||
interface AccountInfo {
|
||||
name: string;
|
||||
photo: Record<string, any>[];
|
||||
country: string;
|
||||
language: string;
|
||||
}
|
||||
|
||||
interface SearchOptions {
|
||||
client: 'YTMUSIC' | 'YOUTUBE';
|
||||
period: 'any' | 'hour' | 'day' | 'week' | 'month' | 'year';
|
||||
order: 'relevance' | 'rating' | 'age' | 'views';
|
||||
duration: 'any' | 'short' | 'long';
|
||||
}
|
||||
|
||||
interface YouTubeSearch {
|
||||
query: string;
|
||||
corrected_query: string;
|
||||
estimated_results: number;
|
||||
videos: any[];
|
||||
getContinuation: () => Promise<object>;
|
||||
}
|
||||
|
||||
interface YouTubeMusicSearch {
|
||||
query: string;
|
||||
corrected_query: string;
|
||||
results: {
|
||||
top_result?: any[];
|
||||
songs?: any[];
|
||||
albums?: any[];
|
||||
videos?: any[];
|
||||
community_playlists?: any[];
|
||||
artists?: any[];
|
||||
}
|
||||
}
|
||||
|
||||
type SearchResults = YouTubeSearch | YouTubeMusicSearch;
|
||||
|
||||
type ClientOption = Pick<SearchOptions, 'client'>;
|
||||
|
||||
type SortBy = 'TOP_COMMENTS' | 'NEWEST_FIRST';
|
||||
|
||||
interface Suggestion {
|
||||
text: string;
|
||||
bold_text: string;
|
||||
}
|
||||
|
||||
interface ApiStatus {
|
||||
success: boolean;
|
||||
status_code: number;
|
||||
data: object;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface Comments {
|
||||
page_count: number,
|
||||
comment_count: number;
|
||||
items: any[];
|
||||
getContinuation: () => Promise<object>;
|
||||
}
|
||||
|
||||
interface Video {
|
||||
title: string;
|
||||
description: string;
|
||||
thumbnail: object;
|
||||
metadata: Record<any, any>;
|
||||
like: () => Promise<ApiStatus>;
|
||||
dislike: () => Promise<ApiStatus>;
|
||||
removeLike: () => Promise<ApiStatus>;
|
||||
subscribe: () => Promise<ApiStatus>;
|
||||
unsubscribe: () => Promise<ApiStatus>;
|
||||
comment: (text: string) => Promise<ApiStatus>;
|
||||
getComments: () => Promise<Comments>;
|
||||
getLivechat: () => any; // TODO type LiveChat
|
||||
setNotificationPreferences: () => Promise<ApiStatus>;
|
||||
}
|
||||
|
||||
interface Channel {
|
||||
title: string;
|
||||
description: string;
|
||||
metadata: object;
|
||||
content: object;
|
||||
}
|
||||
|
||||
interface PlayList {
|
||||
description: string;
|
||||
items: any[];
|
||||
title: string;
|
||||
total_items: string | number;
|
||||
duration?: string;
|
||||
last_updated?: string;
|
||||
views?: string;
|
||||
year?: string;
|
||||
}
|
||||
|
||||
interface History {
|
||||
items: {
|
||||
date: string;
|
||||
videos: any[];
|
||||
}[];
|
||||
getContinuation: () => Promise<object>;
|
||||
}
|
||||
|
||||
interface SubscriptionFeed {
|
||||
items: {
|
||||
date: string;
|
||||
videos: any[];
|
||||
}[];
|
||||
getContinuation: () => Promise<object>;
|
||||
}
|
||||
|
||||
interface HomeFeed {
|
||||
videos: {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
channel: string;
|
||||
metadata: Record<string, any>;
|
||||
}[];
|
||||
getContinuation: () => Promise<object>;
|
||||
}
|
||||
|
||||
interface Trending {
|
||||
now: {
|
||||
content: {
|
||||
title: string;
|
||||
videos: [];
|
||||
}[];
|
||||
};
|
||||
music: { getVideos: () => Promise<Array>; };
|
||||
gaming: { getVideos: () => Promise<Array>; };
|
||||
movies: { getVideos: () => Promise<Array>; };
|
||||
}
|
||||
|
||||
interface Notifications {
|
||||
items: {
|
||||
title: string;
|
||||
sent_time: string;
|
||||
channel_name: string;
|
||||
channel_thumbnail: Record<string, any>;
|
||||
video_thumbnail: Record<string, any>;
|
||||
video_url: string;
|
||||
read: boolean;
|
||||
notification_id: string;
|
||||
}[];
|
||||
getContinuation: () => Promise<object>;
|
||||
}
|
||||
|
||||
interface StreamingData {
|
||||
selected_format: Record<string, any>;
|
||||
formats: any[];
|
||||
}
|
||||
interface StreamingOptions {
|
||||
quality?: string;
|
||||
type?: string;
|
||||
format?: string;
|
||||
}
|
||||
|
||||
interface Config {
|
||||
gl?: string;
|
||||
cookie?: string;
|
||||
}
|
||||
|
||||
export default class Innertube {
|
||||
constructor(auth_info?: Config)
|
||||
|
||||
public signIn(auth_info: AuthInfo): Promise<void>;
|
||||
public signOut(): Promise<ApiStatus>;
|
||||
public getAccountInfo(): Promise<AccountInfo>;
|
||||
public search(query: string, options: SearchOptions): Promise<SearchResults>;
|
||||
public getSearchSuggestions(query: string, options?: ClientOption): Promise<Suggestion>;
|
||||
public getDetails(video_id: string): Promise<ApiStatus>;
|
||||
public getChannel(id: string): Promise<Channel>;
|
||||
public getLyrics(video_id: string): Promise<string>;
|
||||
public getPlaylist(playlist_id: string, options?: ClientOption): Promise<PlayList>;
|
||||
public getComments(video_id: string, sort_by?: SortBy): Promise<Comments>;
|
||||
public getHistory(): Promise<History>;
|
||||
public getHomeFeed(): Promise<HomeFeed>;
|
||||
public getTrending(): Promise<Trending>;
|
||||
public getSubscriptionsFeed(): Promise<SubscriptionFeed>;
|
||||
public getNotifications(): Promise<Notifications>;
|
||||
public getUnseenNotificationsCount(): Promise<number>;
|
||||
public getStreamingData(id: string, options?: StreamingOptions): Promise<StreamingData>;
|
||||
public download(id: string, options?: StreamingOptions): ReadableStream;
|
||||
}
|
||||
declare const _exports: typeof import("./lib/Innertube");
|
||||
export = _exports;
|
||||
|
||||
330
typings/lib/Innertube.d.ts
vendored
Normal file
330
typings/lib/Innertube.d.ts
vendored
Normal file
@@ -0,0 +1,330 @@
|
||||
export = Innertube;
|
||||
/**
|
||||
* Innertube instance.
|
||||
* @namespace
|
||||
*/
|
||||
declare class Innertube {
|
||||
/**
|
||||
* @example
|
||||
* ```js
|
||||
* const Innertube = require('youtubei.js');
|
||||
* const youtube = await new Innertube();
|
||||
* ```
|
||||
*
|
||||
* @param {object} [config]
|
||||
* @param {string} [config.gl]
|
||||
* @param {string} [config.cookie]
|
||||
* @param {boolean} [config.debug]
|
||||
*
|
||||
* @returns {Innertube}
|
||||
* @constructor
|
||||
*/
|
||||
constructor(config?: {
|
||||
gl?: string;
|
||||
cookie?: string;
|
||||
debug?: boolean;
|
||||
});
|
||||
config: {
|
||||
gl?: string;
|
||||
cookie?: string;
|
||||
debug?: boolean;
|
||||
};
|
||||
key: any;
|
||||
version: any;
|
||||
context: any;
|
||||
logged_in: boolean;
|
||||
player_url: any;
|
||||
sts: any;
|
||||
/**
|
||||
* @fires Innertube#auth - fired when signing in to an account.
|
||||
* @fires Innertube#update-credentials - fired when the access token is no longer valid.
|
||||
* @type {EventEmitter}
|
||||
*/
|
||||
ev: EventEmitter;
|
||||
oauth: OAuth;
|
||||
auth_apisid: any;
|
||||
request: Request;
|
||||
actions: Actions;
|
||||
account: AccountManager;
|
||||
playlist: PlaylistManager;
|
||||
interact: InteractionManager;
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @returns {Promise.<void>}
|
||||
*/
|
||||
signIn(auth_info?: {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
expires: Date;
|
||||
}): Promise<void>;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
/**
|
||||
* Signs out of an account.
|
||||
* @returns {Promise.<{ success: boolean; status_code: number }>}
|
||||
*/
|
||||
signOut(): Promise<{
|
||||
success: boolean;
|
||||
status_code: number;
|
||||
}>;
|
||||
/**
|
||||
* Searches a given query.
|
||||
*
|
||||
* @param {string} query - search query.
|
||||
* @param {object} [options] - search options.
|
||||
* @param {string} [options.client] - client used to perform the search, can be: `YTMUSIC` or `YOUTUBE`.
|
||||
* @param {object} [options.filters] - search filters.
|
||||
* @param {string} [options.filters.upload_date] - filter videos by upload date, can be: any | last_hour | today | this_week | this_month | this_year
|
||||
* @param {string} [options.filters.type] - filter results by type, can be: any | video | channel | playlist | movie
|
||||
* @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: object[] } |
|
||||
* { results: { songs: object[]; videos: object[]; albums: object[]; community_playlists: object[] } }>}
|
||||
*/
|
||||
search(query: string, options?: {
|
||||
client?: string;
|
||||
filters?: {
|
||||
upload_date?: string;
|
||||
type?: string;
|
||||
duration?: string;
|
||||
sort_by?: string;
|
||||
};
|
||||
}): Promise<{
|
||||
query: string;
|
||||
corrected_query: string;
|
||||
estimated_results: number;
|
||||
videos: object[];
|
||||
} | {
|
||||
results: {
|
||||
songs: object[];
|
||||
videos: object[];
|
||||
albums: object[];
|
||||
community_playlists: object[];
|
||||
};
|
||||
}>;
|
||||
/**
|
||||
* Retrieves search suggestions for a given 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.<{ query: string; results: string[] }>}
|
||||
*/
|
||||
getSearchSuggestions(query: string, options?: {
|
||||
client?: string;
|
||||
}): Promise<{
|
||||
query: string;
|
||||
results: string[];
|
||||
}>;
|
||||
/**
|
||||
* Retrieves video info.
|
||||
* @param {string} video_id - the video id.
|
||||
* @return {Promise.<{ title: string; description: string; thumbnail: []; metadata: object }>}
|
||||
*/
|
||||
getDetails(video_id: string): Promise<{
|
||||
title: string;
|
||||
description: string;
|
||||
thumbnail: [];
|
||||
metadata: object;
|
||||
}>;
|
||||
/**
|
||||
* Retrieves comments for a given video.
|
||||
*
|
||||
* @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: object[]; }>}
|
||||
*/
|
||||
getComments(video_id: string, sort_by?: string): Promise<{
|
||||
page_count: number;
|
||||
comment_count: number;
|
||||
items: object[];
|
||||
}>;
|
||||
/**
|
||||
* Retrieves contents for a given channel. (WIP)
|
||||
* @param {string} id - channel id
|
||||
* @return {Promise.<{ title: string; description: string; metadata: object; content: object }>}
|
||||
*/
|
||||
getChannel(id: string): Promise<{
|
||||
title: string;
|
||||
description: string;
|
||||
metadata: object;
|
||||
content: object;
|
||||
}>;
|
||||
/**
|
||||
* Retrieves watch history.
|
||||
* @returns {Promise.<{ items: { date: string; videos: object[] }[] }>}
|
||||
*/
|
||||
getHistory(): Promise<{
|
||||
items: {
|
||||
date: string;
|
||||
videos: object[];
|
||||
}[];
|
||||
}>;
|
||||
/**
|
||||
* Retrieves home feed (aka recommendations).
|
||||
* @returns {Promise.<{ videos: { id: string; title: string; description: string; channel: string; metadata: object }[] }>}
|
||||
*/
|
||||
getHomeFeed(): Promise<{
|
||||
videos: {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
channel: string;
|
||||
metadata: object;
|
||||
}[];
|
||||
}>;
|
||||
/**
|
||||
* Retrieves trending content.
|
||||
* @returns {Promise.<{ now: { content: { title: string; videos: object[]; }[] };
|
||||
* music: { getVideos: Promise.<Array.<object>>; }; gaming: { getVideos: Promise.<Array.<object>>; };
|
||||
* movies: { getVideos: Promise.<Array.<object>>; } }>}
|
||||
*/
|
||||
getTrending(): Promise<{
|
||||
now: {
|
||||
content: {
|
||||
title: string;
|
||||
videos: object[];
|
||||
}[];
|
||||
};
|
||||
music: {
|
||||
getVideos: Promise<Array<object>>;
|
||||
};
|
||||
gaming: {
|
||||
getVideos: Promise<Array<object>>;
|
||||
};
|
||||
movies: {
|
||||
getVideos: Promise<Array<object>>;
|
||||
};
|
||||
}>;
|
||||
/**
|
||||
* @todo finish this
|
||||
* WIP
|
||||
*/
|
||||
getLibrary(): Promise<any>;
|
||||
/**
|
||||
* Retrieves subscriptions feed.
|
||||
* @returns {Promise.<{ items: { date: string; videos: object[] }[] }>}
|
||||
*/
|
||||
getSubscriptionsFeed(): Promise<{
|
||||
items: {
|
||||
date: string;
|
||||
videos: object[];
|
||||
}[];
|
||||
}>;
|
||||
/**
|
||||
* 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: {
|
||||
title: string;
|
||||
sent_time: string;
|
||||
channel_name: string;
|
||||
channel_thumbnail: object;
|
||||
video_thumbnail: object;
|
||||
video_url: string;
|
||||
read: boolean;
|
||||
notification_id: string;
|
||||
}[];
|
||||
}>;
|
||||
/**
|
||||
* Retrieves unseen notifications count.
|
||||
* @returns {Promise.<number>}
|
||||
*/
|
||||
getUnseenNotificationsCount(): Promise<number>;
|
||||
/**
|
||||
* Retrieves lyrics for a given song if available.
|
||||
*
|
||||
* @param {string} video_id
|
||||
* @returns {Promise.<string>}
|
||||
*/
|
||||
getLyrics(video_id: string): Promise<string>;
|
||||
/**
|
||||
* Retrieves the contents of a given playlist.
|
||||
*
|
||||
* @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: [] }>}
|
||||
*/
|
||||
getPlaylist(playlist_id: string, options?: {
|
||||
client: string;
|
||||
}): 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: [];
|
||||
}>;
|
||||
/**
|
||||
* An alternative to {@link download}.
|
||||
* Returns deciphered streaming data.
|
||||
*
|
||||
* @param {string} video_id - video id
|
||||
* @param {object} options - download options.
|
||||
* @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
|
||||
*
|
||||
* @returns {Promise.<{ selected_format: object; formats: object[] }>}
|
||||
*/
|
||||
getStreamingData(video_id: string, options?: {
|
||||
quality: string;
|
||||
type: string;
|
||||
format: string;
|
||||
}): Promise<{
|
||||
selected_format: object;
|
||||
formats: object[];
|
||||
}>;
|
||||
/**
|
||||
* Downloads a given video. If you only need the direct download link take a look at {@link getStreamingData}.
|
||||
*
|
||||
* @param {string} video_id - video id
|
||||
* @param {object} options - download options.
|
||||
* @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}
|
||||
*/
|
||||
download(video_id: string, options?: {
|
||||
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");
|
||||
192
typings/lib/core/AccountManager.d.ts
vendored
Normal file
192
typings/lib/core/AccountManager.d.ts
vendored
Normal file
@@ -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;
|
||||
}
|
||||
331
typings/lib/core/Actions.d.ts
vendored
Normal file
331
typings/lib/core/Actions.d.ts
vendored
Normal file
@@ -0,0 +1,331 @@
|
||||
export = Actions;
|
||||
/** namespace **/
|
||||
declare class Actions {
|
||||
/**
|
||||
* @param {Innertube} session
|
||||
* @constructor
|
||||
*/
|
||||
constructor(session: Innertube);
|
||||
/**
|
||||
* Covers `/browse` endpoint, mostly used to access
|
||||
* YouTube's sections such as the home feed, etc
|
||||
* and sometimes to retrieve continuations.
|
||||
*
|
||||
* @param {string} id - browseId or a continuation token
|
||||
* @param {object} args - additional arguments
|
||||
* @param {string} [args.params]
|
||||
* @param {boolean} [args.is_ytm]
|
||||
* @param {boolean} [args.is_ctoken]
|
||||
* @param {string} [args.client]
|
||||
*
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object }>}
|
||||
*/
|
||||
browse(id: string, args?: {
|
||||
params?: string;
|
||||
is_ytm?: boolean;
|
||||
is_ctoken?: boolean;
|
||||
client?: string;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
status_code: number;
|
||||
data: object;
|
||||
}>;
|
||||
/**
|
||||
* Covers endpoints used to perform direct interactions
|
||||
* on YouTube.
|
||||
*
|
||||
* @param {string} action
|
||||
* @param {object} args
|
||||
* @param {string} [args.video_id]
|
||||
* @param {string} [args.channel_id]
|
||||
* @param {string} [args.comment_id]
|
||||
* @param {string} [args.comment_action]
|
||||
*
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
engage(action: string, args?: {
|
||||
video_id?: string;
|
||||
channel_id?: string;
|
||||
comment_id?: string;
|
||||
comment_action?: string;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
status_code: number;
|
||||
data: object;
|
||||
}>;
|
||||
/**
|
||||
* Covers endpoints related to account management.
|
||||
*
|
||||
* @param {string} action
|
||||
* @param {object} args
|
||||
* @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;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
status_code: number;
|
||||
data: object;
|
||||
}>;
|
||||
/**
|
||||
* Endpoint used for search.
|
||||
*
|
||||
* @param {object} args
|
||||
* @param {string} [args.query]
|
||||
* @param {object} [args.options]
|
||||
* @param {string} [args.options.period]
|
||||
* @param {string} [args.options.duration]
|
||||
* @param {string} [args.options.order]
|
||||
* @param {string} [args.client]
|
||||
* @param {string} [args.ctoken]
|
||||
*
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
search(args?: {
|
||||
query?: string;
|
||||
options?: {
|
||||
period?: string;
|
||||
duration?: string;
|
||||
order?: string;
|
||||
};
|
||||
client?: string;
|
||||
ctoken?: string;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
status_code: number;
|
||||
data: object;
|
||||
}>;
|
||||
/**
|
||||
* Endpoint used fo Shorts' sound search.
|
||||
*
|
||||
* @param {object} args
|
||||
* @param {string} args.query
|
||||
*
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
searchSound(args?: {
|
||||
query: string;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
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.
|
||||
*
|
||||
* @param {string} action
|
||||
* @param {object} args
|
||||
* @param {string} [args.title]
|
||||
* @param {string} [args.ids]
|
||||
* @param {string} [args.playlist_id]
|
||||
* @param {string} [args.action]
|
||||
*
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
playlist(action: string, args?: {
|
||||
title?: string;
|
||||
ids?: string;
|
||||
playlist_id?: string;
|
||||
action?: string;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
status_code: number;
|
||||
data: object;
|
||||
}>;
|
||||
/**
|
||||
* Covers endpoints used for notifications management.
|
||||
*
|
||||
* @param {string} action
|
||||
* @param {object} args
|
||||
* @param {string} [args.pref]
|
||||
* @param {string} [args.channel_id]
|
||||
* @param {string} [args.ctoken]
|
||||
*
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
notifications(action: string, args?: {
|
||||
pref?: string;
|
||||
channel_id?: string;
|
||||
ctoken?: string;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
status_code: number;
|
||||
data: object;
|
||||
}>;
|
||||
/**
|
||||
* Covers livechat endpoints.
|
||||
*
|
||||
* @param {string} action
|
||||
* @param {object} args
|
||||
* @param {string} [args.text]
|
||||
* @param {string} [args.video_id]
|
||||
* @param {string} [args.channel_id]
|
||||
* @param {string} [args.ctoken]
|
||||
* @param {string} [args.params]
|
||||
*
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
livechat(action: string, args?: {
|
||||
text?: string;
|
||||
video_id?: string;
|
||||
channel_id?: string;
|
||||
ctoken?: string;
|
||||
params?: string;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
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.
|
||||
*
|
||||
* @param {string} action
|
||||
* @param {object} args
|
||||
* @param {object} [args.action]
|
||||
* @param {string} [args.params]
|
||||
*
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
flag(action: string, args: {
|
||||
action?: object;
|
||||
params?: string;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
status_code: number;
|
||||
data: object;
|
||||
}>;
|
||||
/**
|
||||
* Covers specific YouTube Music endpoints.
|
||||
*
|
||||
* @param {string} action
|
||||
* @param {string} args.input
|
||||
*
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
music(action: string, args: any): Promise<{
|
||||
success: boolean;
|
||||
status_code: number;
|
||||
data: object;
|
||||
}>;
|
||||
/**
|
||||
* Mostly used for pagination and specific operations.
|
||||
*
|
||||
* @param {object} args
|
||||
* @param {string} [args.video_id]
|
||||
* @param {string} [args.ctoken]
|
||||
* @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;
|
||||
data: object;
|
||||
}>;
|
||||
/**
|
||||
* Used to retrieve video info.
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {string} [cpn]
|
||||
*
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
getVideoInfo(id: string, cpn?: string): Promise<{
|
||||
success: boolean;
|
||||
status_code: number;
|
||||
data: object;
|
||||
}>;
|
||||
/**
|
||||
* Covers search suggestion endpoints.
|
||||
*
|
||||
* @param {string} client
|
||||
* @param {string} input
|
||||
*
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; }>}
|
||||
*/
|
||||
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;
|
||||
}>;
|
||||
#private;
|
||||
}
|
||||
107
typings/lib/core/InteractionManager.d.ts
vendored
Normal file
107
typings/lib/core/InteractionManager.d.ts
vendored
Normal file
@@ -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;
|
||||
}
|
||||
23
typings/lib/core/Livechat.d.ts
vendored
Normal file
23
typings/lib/core/Livechat.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
export = Livechat;
|
||||
declare class Livechat extends EventEmitter {
|
||||
constructor(session: any, token: any, channel_id: any, video_id: any);
|
||||
ctoken: any;
|
||||
session: any;
|
||||
video_id: any;
|
||||
channel_id: any;
|
||||
message_queue: any[];
|
||||
id_cache: any[];
|
||||
poll_intervals_ms: number;
|
||||
running: boolean;
|
||||
metadata_ctoken: any;
|
||||
livechat_poller: NodeJS.Timeout;
|
||||
sendMessage(text: any): Promise<any>;
|
||||
/**
|
||||
* Blocks a user.
|
||||
* @todo Implement this method
|
||||
*/
|
||||
blockUser(): Promise<void>;
|
||||
stop(): void;
|
||||
#private;
|
||||
}
|
||||
import EventEmitter = require("events");
|
||||
47
typings/lib/core/OAuth.d.ts
vendored
Normal file
47
typings/lib/core/OAuth.d.ts
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
export = OAuth;
|
||||
/** @namespace */
|
||||
declare class OAuth {
|
||||
/**
|
||||
* @param {EventEmitter} ev
|
||||
* @constructor
|
||||
*/
|
||||
constructor(ev: EventEmitter);
|
||||
/**
|
||||
* Starts the auth flow in case no valid credentials are available.
|
||||
* @returns {Promise.<void>}
|
||||
*/
|
||||
init(auth_info: any): Promise<void>;
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
/**
|
||||
* Refreshes the access token if necessary.
|
||||
* @returns {Promise.<void>}
|
||||
*/
|
||||
checkTokenValidity(): Promise<void>;
|
||||
/**
|
||||
* Revokes access token (note that the refresh token will also be revoked).
|
||||
* @returns {Promise.<void>}
|
||||
*/
|
||||
revokeAccessToken(): Promise<void>;
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* Checks access token validity.
|
||||
* @returns {boolean} true | false
|
||||
*/
|
||||
shouldRefreshToken(): boolean;
|
||||
#private;
|
||||
}
|
||||
37
typings/lib/core/Player.d.ts
vendored
Normal file
37
typings/lib/core/Player.d.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
export = Player;
|
||||
/** @namespace */
|
||||
declare class Player {
|
||||
/**
|
||||
* Represents the YouTube Web player script.
|
||||
* @param {string} id - the id of the player.
|
||||
* @constructor
|
||||
*/
|
||||
constructor(id: string);
|
||||
init(): Promise<Player>;
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
63
typings/lib/core/PlaylistManager.d.ts
vendored
Normal file
63
typings/lib/core/PlaylistManager.d.ts
vendored
Normal file
@@ -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.<string>} video_ids
|
||||
*
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; playlist_id: string; data: object; }>}
|
||||
*/
|
||||
create(title: string, video_ids: Array<string>): 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.<string>} video_ids
|
||||
*
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; playlist_id: string; data: object; }>}
|
||||
*/
|
||||
addVideos(playlist_id: string, video_ids: Array<string>): Promise<{
|
||||
success: boolean;
|
||||
status_code: number;
|
||||
playlist_id: string;
|
||||
data: object;
|
||||
}>;
|
||||
/**
|
||||
* 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; }>}
|
||||
*/
|
||||
removeVideos(playlist_id: string, video_ids: Array<string>): Promise<{
|
||||
success: boolean;
|
||||
status_code: number;
|
||||
playlist_id: string;
|
||||
data: object;
|
||||
}>;
|
||||
#private;
|
||||
}
|
||||
23
typings/lib/core/SessionBuilder.d.ts
vendored
Normal file
23
typings/lib/core/SessionBuilder.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
export = SessionBuilder;
|
||||
/** @namespace */
|
||||
declare class SessionBuilder {
|
||||
/**
|
||||
* @param {object} config
|
||||
* @constructor
|
||||
*/
|
||||
constructor(config: object);
|
||||
build(): Promise<SessionBuilder>;
|
||||
/** @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;
|
||||
}
|
||||
12
typings/lib/deciphers/NToken.d.ts
vendored
Normal file
12
typings/lib/deciphers/NToken.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
export = NToken;
|
||||
declare class NToken {
|
||||
constructor(raw_code: any, n: any);
|
||||
n: any;
|
||||
raw_code: any;
|
||||
/**
|
||||
* Solves throttling challange by transforming the n token.
|
||||
* @returns {string}
|
||||
*/
|
||||
transform(): string;
|
||||
#private;
|
||||
}
|
||||
12
typings/lib/deciphers/Signature.d.ts
vendored
Normal file
12
typings/lib/deciphers/Signature.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
export = Signature;
|
||||
declare class Signature {
|
||||
constructor(url: any, sig_decipher_sc: any);
|
||||
url: any;
|
||||
sig_decipher_sc: any;
|
||||
/**
|
||||
* Deciphers signature.
|
||||
* @returns {string}
|
||||
*/
|
||||
decipher(): string;
|
||||
#private;
|
||||
}
|
||||
9
typings/lib/parser/index.d.ts
vendored
Normal file
9
typings/lib/parser/index.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
export = Parser;
|
||||
declare class Parser {
|
||||
constructor(session: any, data: any, args?: {});
|
||||
data: any;
|
||||
session: any;
|
||||
args: {};
|
||||
parse(): any;
|
||||
#private;
|
||||
}
|
||||
11
typings/lib/parser/youtube/index.d.ts
vendored
Normal file
11
typings/lib/parser/youtube/index.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import VideoResultItem = require("./search/VideoResultItem");
|
||||
import SearchSuggestionItem = require("./search/SearchSuggestionItem");
|
||||
import PlaylistItem = require("./others/PlaylistItem");
|
||||
import NotificationItem = require("./others/NotificationItem");
|
||||
import VideoItem = require("./others/VideoItem");
|
||||
import GridVideoItem = require("./others/GridVideoItem");
|
||||
import GridPlaylistItem = require("./others/GridPlaylistItem");
|
||||
import ChannelMetadata = require("./others/ChannelMetadata");
|
||||
import ShelfRenderer = require("./others/ShelfRenderer");
|
||||
import CommentThread = require("./others/CommentThread");
|
||||
export { VideoResultItem, SearchSuggestionItem, PlaylistItem, NotificationItem, VideoItem, GridVideoItem, GridPlaylistItem, ChannelMetadata, ShelfRenderer, CommentThread };
|
||||
15
typings/lib/parser/youtube/others/ChannelMetadata.d.ts
vendored
Normal file
15
typings/lib/parser/youtube/others/ChannelMetadata.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
export = ChannelMetadata;
|
||||
declare class ChannelMetadata {
|
||||
static parse(data: any): {
|
||||
title: any;
|
||||
description: any;
|
||||
metadata: {
|
||||
url: any;
|
||||
rss_urls: any;
|
||||
vanity_channel_url: any;
|
||||
external_id: any;
|
||||
is_family_safe: any;
|
||||
keywords: any;
|
||||
};
|
||||
};
|
||||
}
|
||||
23
typings/lib/parser/youtube/others/CommentThread.d.ts
vendored
Normal file
23
typings/lib/parser/youtube/others/CommentThread.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
export = CommentThread;
|
||||
declare class CommentThread {
|
||||
static parseItem(item: any): {
|
||||
text: any;
|
||||
author: {
|
||||
name: any;
|
||||
thumbnails: any;
|
||||
channel_id: any;
|
||||
channel_url: string;
|
||||
};
|
||||
metadata: {
|
||||
published: any;
|
||||
is_reply: boolean;
|
||||
is_liked: any;
|
||||
is_disliked: any;
|
||||
is_pinned: boolean;
|
||||
is_channel_owner: any;
|
||||
like_count: number;
|
||||
reply_count: any;
|
||||
id: any;
|
||||
};
|
||||
};
|
||||
}
|
||||
12
typings/lib/parser/youtube/others/GridPlaylistItem.d.ts
vendored
Normal file
12
typings/lib/parser/youtube/others/GridPlaylistItem.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
export = GridPlaylistItem;
|
||||
declare class GridPlaylistItem {
|
||||
static parse(data: any): any;
|
||||
static parseItem(item: any): {
|
||||
id: any;
|
||||
title: any;
|
||||
metadata: {
|
||||
thumbnail: any;
|
||||
video_count: any;
|
||||
};
|
||||
};
|
||||
}
|
||||
25
typings/lib/parser/youtube/others/GridVideoItem.d.ts
vendored
Normal file
25
typings/lib/parser/youtube/others/GridVideoItem.d.ts
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
export = GridVideoItem;
|
||||
declare class GridVideoItem {
|
||||
static parse(data: any): any;
|
||||
static parseItem(item: any): {
|
||||
id: any;
|
||||
title: any;
|
||||
channel: {
|
||||
id: any;
|
||||
name: any;
|
||||
url: string;
|
||||
};
|
||||
metadata: {
|
||||
view_count: any;
|
||||
short_view_count_text: {
|
||||
simple_text: any;
|
||||
accessibility_label: any;
|
||||
};
|
||||
thumbnail: any;
|
||||
moving_thumbnail: any;
|
||||
published: any;
|
||||
badges: any;
|
||||
owner_badges: any;
|
||||
};
|
||||
};
|
||||
}
|
||||
14
typings/lib/parser/youtube/others/NotificationItem.d.ts
vendored
Normal file
14
typings/lib/parser/youtube/others/NotificationItem.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
export = NotificationItem;
|
||||
declare class NotificationItem {
|
||||
static parse(data: any): any;
|
||||
static parseItem(item: any): {
|
||||
title: any;
|
||||
sent_time: any;
|
||||
channel_name: any;
|
||||
channel_thumbnail: any;
|
||||
video_thumbnail: any;
|
||||
video_url: string;
|
||||
read: any;
|
||||
notification_id: any;
|
||||
};
|
||||
}
|
||||
15
typings/lib/parser/youtube/others/PlaylistItem.d.ts
vendored
Normal file
15
typings/lib/parser/youtube/others/PlaylistItem.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
export = PlaylistItem;
|
||||
declare class PlaylistItem {
|
||||
static parse(data: any): any;
|
||||
static parseItem(item: any): {
|
||||
id: any;
|
||||
title: any;
|
||||
author: any;
|
||||
duration: {
|
||||
seconds: number;
|
||||
simple_text: any;
|
||||
accessibility_label: any;
|
||||
};
|
||||
thumbnails: any;
|
||||
};
|
||||
}
|
||||
9
typings/lib/parser/youtube/others/ShelfRenderer.d.ts
vendored
Normal file
9
typings/lib/parser/youtube/others/ShelfRenderer.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
export = ShelfRenderer;
|
||||
declare class ShelfRenderer {
|
||||
static parse(data: any): {
|
||||
title: any;
|
||||
videos: any;
|
||||
};
|
||||
static getTitle(data: any): any;
|
||||
static parseItems(data: any): any;
|
||||
}
|
||||
31
typings/lib/parser/youtube/others/VideoItem.d.ts
vendored
Normal file
31
typings/lib/parser/youtube/others/VideoItem.d.ts
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
export = VideoItem;
|
||||
declare class VideoItem {
|
||||
static parse(data: any): any;
|
||||
static parseItem(item: any): {
|
||||
id: any;
|
||||
title: any;
|
||||
description: any;
|
||||
channel: {
|
||||
id: any;
|
||||
name: any;
|
||||
url: string;
|
||||
};
|
||||
metadata: {
|
||||
view_count: any;
|
||||
short_view_count_text: {
|
||||
simple_text: any;
|
||||
accessibility_label: any;
|
||||
};
|
||||
thumbnail: any;
|
||||
moving_thumbnail: any;
|
||||
published: any;
|
||||
duration: {
|
||||
seconds: number;
|
||||
simple_text: any;
|
||||
accessibility_label: any;
|
||||
};
|
||||
badges: any;
|
||||
owner_badges: any;
|
||||
};
|
||||
};
|
||||
}
|
||||
7
typings/lib/parser/youtube/search/SearchSuggestionItem.d.ts
vendored
Normal file
7
typings/lib/parser/youtube/search/SearchSuggestionItem.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export = SearchSuggestionItem;
|
||||
declare class SearchSuggestionItem {
|
||||
static parse(data: any): {
|
||||
query: any;
|
||||
results: any;
|
||||
};
|
||||
}
|
||||
31
typings/lib/parser/youtube/search/VideoResultItem.d.ts
vendored
Normal file
31
typings/lib/parser/youtube/search/VideoResultItem.d.ts
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
export = VideoResultItem;
|
||||
declare class VideoResultItem {
|
||||
static parse(data: any): any;
|
||||
static parseItem(item: any): {
|
||||
id: any;
|
||||
url: string;
|
||||
title: any;
|
||||
description: any;
|
||||
channel: {
|
||||
id: any;
|
||||
name: any;
|
||||
url: string;
|
||||
};
|
||||
metadata: {
|
||||
view_count: any;
|
||||
short_view_count_text: {
|
||||
simple_text: any;
|
||||
accessibility_label: any;
|
||||
};
|
||||
thumbnails: any;
|
||||
duration: {
|
||||
seconds: number;
|
||||
simple_text: any;
|
||||
accessibility_label: any;
|
||||
};
|
||||
published: any;
|
||||
badges: any;
|
||||
owner_badges: any;
|
||||
};
|
||||
};
|
||||
}
|
||||
9
typings/lib/parser/ytmusic/index.d.ts
vendored
Normal file
9
typings/lib/parser/ytmusic/index.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import SongResultItem = require("./search/SongResultItem");
|
||||
import VideoResultItem = require("./search/VideoResultItem");
|
||||
import AlbumResultItem = require("./search/AlbumResultItem");
|
||||
import ArtistResultItem = require("./search/ArtistResultItem");
|
||||
import PlaylistResultItem = require("./search/PlaylistResultItem");
|
||||
import MusicSearchSuggestionItem = require("./search/MusicSearchSuggestionItem");
|
||||
import TopResultItem = require("./search/TopResultItem");
|
||||
import PlaylistItem = require("./others/PlaylistItem");
|
||||
export { SongResultItem, VideoResultItem, AlbumResultItem, ArtistResultItem, PlaylistResultItem, MusicSearchSuggestionItem, TopResultItem, PlaylistItem };
|
||||
14
typings/lib/parser/ytmusic/others/PlaylistItem.d.ts
vendored
Normal file
14
typings/lib/parser/ytmusic/others/PlaylistItem.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
export = PlaylistItem;
|
||||
declare class PlaylistItem {
|
||||
static parse(data: any): any;
|
||||
static parseItem(item: any): {
|
||||
id: any;
|
||||
title: any;
|
||||
author: any;
|
||||
duration: {
|
||||
seconds: number;
|
||||
simple_text: any;
|
||||
};
|
||||
thumbnails: any;
|
||||
};
|
||||
}
|
||||
11
typings/lib/parser/ytmusic/search/AlbumResultItem.d.ts
vendored
Normal file
11
typings/lib/parser/ytmusic/search/AlbumResultItem.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
export = AlbumResultItem;
|
||||
declare class AlbumResultItem {
|
||||
static parse(data: any): any;
|
||||
static parseItem(item: any): {
|
||||
id: any;
|
||||
title: any;
|
||||
author: any;
|
||||
year: any;
|
||||
thumbnails: any;
|
||||
};
|
||||
}
|
||||
10
typings/lib/parser/ytmusic/search/ArtistResultItem.d.ts
vendored
Normal file
10
typings/lib/parser/ytmusic/search/ArtistResultItem.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
export = ArtistResultItem;
|
||||
declare class ArtistResultItem {
|
||||
static parse(data: any): any;
|
||||
static parseItem(item: any): {
|
||||
id: any;
|
||||
name: any;
|
||||
subscribers: any;
|
||||
thumbnails: any;
|
||||
};
|
||||
}
|
||||
8
typings/lib/parser/ytmusic/search/MusicSearchSuggestionItem.d.ts
vendored
Normal file
8
typings/lib/parser/ytmusic/search/MusicSearchSuggestionItem.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
export = MusicSearchSuggestionItem;
|
||||
declare class MusicSearchSuggestionItem {
|
||||
static parse(data: any): {
|
||||
query: any;
|
||||
results: any;
|
||||
};
|
||||
static parseItem(item: any): any;
|
||||
}
|
||||
11
typings/lib/parser/ytmusic/search/PlaylistResultItem.d.ts
vendored
Normal file
11
typings/lib/parser/ytmusic/search/PlaylistResultItem.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
export = PlaylistResultItem;
|
||||
declare class PlaylistResultItem {
|
||||
static parse(data: any): any;
|
||||
static parseItem(item: any): {
|
||||
id: any;
|
||||
title: any;
|
||||
author: any;
|
||||
channel_id: any;
|
||||
total_items: number;
|
||||
};
|
||||
}
|
||||
12
typings/lib/parser/ytmusic/search/SongResultItem.d.ts
vendored
Normal file
12
typings/lib/parser/ytmusic/search/SongResultItem.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
export = SongResultItem;
|
||||
declare class SongResultItem {
|
||||
static parse(data: any): any;
|
||||
static parseItem(item: any): {
|
||||
id: any;
|
||||
title: any;
|
||||
artist: any;
|
||||
album: any;
|
||||
duration: any;
|
||||
thumbnails: any;
|
||||
};
|
||||
}
|
||||
4
typings/lib/parser/ytmusic/search/TopResultItem.d.ts
vendored
Normal file
4
typings/lib/parser/ytmusic/search/TopResultItem.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
export = TopResultItem;
|
||||
declare class TopResultItem {
|
||||
static parse(data: any): any;
|
||||
}
|
||||
12
typings/lib/parser/ytmusic/search/VideoResultItem.d.ts
vendored
Normal file
12
typings/lib/parser/ytmusic/search/VideoResultItem.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
export = VideoResultItem;
|
||||
declare class VideoResultItem {
|
||||
static parse(data: any): any;
|
||||
static parseItem(item: any): {
|
||||
id: any;
|
||||
title: any;
|
||||
author: any;
|
||||
views: any;
|
||||
duration: any;
|
||||
thumbnails: any;
|
||||
};
|
||||
}
|
||||
113
typings/lib/proto/index.d.ts
vendored
Normal file
113
typings/lib/proto/index.d.ts
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
export = Proto;
|
||||
declare class Proto {
|
||||
/**
|
||||
* Encodes visitor data.
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {number} timestamp
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
static encodeVisitorData(id: string, timestamp: number): string;
|
||||
/**
|
||||
* Encodes basic channel analytics parameters.
|
||||
*
|
||||
* @param {string} channel_id
|
||||
* @returns {string}
|
||||
*/
|
||||
static encodeChannelAnalyticsParams(channel_id: string): string;
|
||||
/**
|
||||
* Encodes search filters.
|
||||
*
|
||||
* @param {object} filters
|
||||
* @param {string} [filters.upload_date] - any | last_hour | today | this_week | this_month | this_year
|
||||
* @param {string} [filters.type] - any | video | channel | playlist | movie
|
||||
* @param {string} [filters.duration] - any | short | medium | long
|
||||
* @param {string} [filters.sort_by] - relevance | rating | upload_date | view_count
|
||||
* @todo implement remaining filters.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
static encodeSearchFilter(filters: {
|
||||
upload_date?: string;
|
||||
type?: string;
|
||||
duration?: string;
|
||||
sort_by?: string;
|
||||
}): string;
|
||||
/**
|
||||
* Encodes livechat message parameters.
|
||||
*
|
||||
* @param {string} channel_id
|
||||
* @param {string} video_id
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
static encodeMessageParams(channel_id: string, video_id: string): string;
|
||||
/**
|
||||
* Encodes comment section parameters.
|
||||
*
|
||||
* @param {string} video_id
|
||||
* @param {object} options
|
||||
* @param {string} options.type
|
||||
* @param {string} options.sort_by
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
static encodeCommentsSectionParams(video_id: string, options?: {
|
||||
type: string;
|
||||
sort_by: string;
|
||||
}): string;
|
||||
/**
|
||||
* Encodes replies thread parameters.
|
||||
*
|
||||
* @param {string} video_id
|
||||
* @param {string} comment_id
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
static encodeCommentRepliesParams(video_id: string, comment_id: string): string;
|
||||
/**
|
||||
* Encodes comment parameters.
|
||||
*
|
||||
* @param {string} video_id
|
||||
* @returns {string}
|
||||
*/
|
||||
static encodeCommentParams(video_id: string): string;
|
||||
/**
|
||||
* Encodes comment reply parameters.
|
||||
*
|
||||
* @param {string} comment_id
|
||||
* @param {string} video_id
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
static encodeCommentReplyParams(comment_id: string, video_id: string): string;
|
||||
/**
|
||||
* Encodes comment action parameters.
|
||||
*
|
||||
* @param {string} type
|
||||
* @param {string} comment_id
|
||||
* @param {string} video_id
|
||||
* @param {string} [text]
|
||||
* @param {string} [target_language]
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
static encodeCommentActionParams(type: string, args?: {}): string;
|
||||
/**
|
||||
* Encodes notification preference parameters.
|
||||
*
|
||||
* @param {string} channel_id
|
||||
* @param {number} index
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
static encodeNotificationPref(channel_id: string, index: number): string;
|
||||
/**
|
||||
* Encodes sound info parameters.
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {string}
|
||||
*/
|
||||
static encodeSoundInfoParams(id: string): string;
|
||||
}
|
||||
106
typings/lib/proto/messages.d.ts
vendored
Normal file
106
typings/lib/proto/messages.d.ts
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
export namespace VisitorData {
|
||||
const buffer: boolean;
|
||||
const encodingLength: any;
|
||||
const encode: any;
|
||||
const decode: any;
|
||||
}
|
||||
export namespace ChannelAnalytics {
|
||||
const buffer_1: boolean;
|
||||
export { buffer_1 as buffer };
|
||||
const encodingLength_1: any;
|
||||
export { encodingLength_1 as encodingLength };
|
||||
const encode_1: any;
|
||||
export { encode_1 as encode };
|
||||
const decode_1: any;
|
||||
export { decode_1 as decode };
|
||||
}
|
||||
export namespace InnertubePayload {
|
||||
const buffer_2: boolean;
|
||||
export { buffer_2 as buffer };
|
||||
const encodingLength_2: any;
|
||||
export { encodingLength_2 as encodingLength };
|
||||
const encode_2: any;
|
||||
export { encode_2 as encode };
|
||||
const decode_2: any;
|
||||
export { decode_2 as decode };
|
||||
}
|
||||
export namespace SoundInfoParams {
|
||||
const buffer_3: boolean;
|
||||
export { buffer_3 as buffer };
|
||||
const encodingLength_3: any;
|
||||
export { encodingLength_3 as encodingLength };
|
||||
const encode_3: any;
|
||||
export { encode_3 as encode };
|
||||
const decode_3: any;
|
||||
export { decode_3 as decode };
|
||||
}
|
||||
export namespace NotificationPreferences {
|
||||
const buffer_4: boolean;
|
||||
export { buffer_4 as buffer };
|
||||
const encodingLength_4: any;
|
||||
export { encodingLength_4 as encodingLength };
|
||||
const encode_4: any;
|
||||
export { encode_4 as encode };
|
||||
const decode_4: any;
|
||||
export { decode_4 as decode };
|
||||
}
|
||||
export namespace LiveMessageParams {
|
||||
const buffer_5: boolean;
|
||||
export { buffer_5 as buffer };
|
||||
const encodingLength_5: any;
|
||||
export { encodingLength_5 as encodingLength };
|
||||
const encode_5: any;
|
||||
export { encode_5 as encode };
|
||||
const decode_5: any;
|
||||
export { decode_5 as decode };
|
||||
}
|
||||
export namespace GetCommentsSectionParams {
|
||||
const buffer_6: boolean;
|
||||
export { buffer_6 as buffer };
|
||||
const encodingLength_6: any;
|
||||
export { encodingLength_6 as encodingLength };
|
||||
const encode_6: any;
|
||||
export { encode_6 as encode };
|
||||
const decode_6: any;
|
||||
export { decode_6 as decode };
|
||||
}
|
||||
export namespace CreateCommentParams {
|
||||
const buffer_7: boolean;
|
||||
export { buffer_7 as buffer };
|
||||
const encodingLength_7: any;
|
||||
export { encodingLength_7 as encodingLength };
|
||||
const encode_7: any;
|
||||
export { encode_7 as encode };
|
||||
const decode_7: any;
|
||||
export { decode_7 as decode };
|
||||
}
|
||||
export namespace CreateCommentReplyParams {
|
||||
const buffer_8: boolean;
|
||||
export { buffer_8 as buffer };
|
||||
const encodingLength_8: any;
|
||||
export { encodingLength_8 as encodingLength };
|
||||
const encode_8: any;
|
||||
export { encode_8 as encode };
|
||||
const decode_8: any;
|
||||
export { decode_8 as decode };
|
||||
}
|
||||
export namespace PeformCommentActionParams {
|
||||
const buffer_9: boolean;
|
||||
export { buffer_9 as buffer };
|
||||
const encodingLength_9: any;
|
||||
export { encodingLength_9 as encodingLength };
|
||||
const encode_9: any;
|
||||
export { encode_9 as encode };
|
||||
const decode_9: any;
|
||||
export { decode_9 as decode };
|
||||
}
|
||||
export namespace SearchFilter {
|
||||
const buffer_10: boolean;
|
||||
export { buffer_10 as buffer };
|
||||
const encodingLength_10: any;
|
||||
export { encodingLength_10 as encodingLength };
|
||||
const encode_10: any;
|
||||
export { encode_10 as encode };
|
||||
const decode_10: any;
|
||||
export { decode_10 as decode };
|
||||
}
|
||||
94
typings/lib/utils/Constants.d.ts
vendored
Normal file
94
typings/lib/utils/Constants.d.ts
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
export namespace URLS {
|
||||
const YT_BASE: string;
|
||||
const YT_BASE_API: string;
|
||||
const YT_STUDIO_BASE_API: string;
|
||||
const YT_SUGGESTIONS: string;
|
||||
const YT_MUSIC: string;
|
||||
const YT_MUSIC_BASE_API: string;
|
||||
}
|
||||
export namespace OAUTH {
|
||||
const SCOPE: string;
|
||||
const GRANT_TYPE: string;
|
||||
const MODEL_NAME: string;
|
||||
namespace HEADERS {
|
||||
const headers: {
|
||||
accept: string;
|
||||
origin: string;
|
||||
'user-agent': string;
|
||||
'content-type': string;
|
||||
referer: string;
|
||||
'accept-language': string;
|
||||
};
|
||||
}
|
||||
namespace REGEX {
|
||||
const AUTH_SCRIPT: RegExp;
|
||||
const CLIENT_IDENTITY: RegExp;
|
||||
}
|
||||
}
|
||||
export namespace CLIENTS {
|
||||
namespace WEB {
|
||||
const NAME: string;
|
||||
}
|
||||
namespace YTMUSIC {
|
||||
const NAME_1: string;
|
||||
export { NAME_1 as NAME };
|
||||
export const VERSION: string;
|
||||
}
|
||||
namespace ANDROID {
|
||||
const NAME_2: string;
|
||||
export { NAME_2 as NAME };
|
||||
const VERSION_1: string;
|
||||
export { VERSION_1 as VERSION };
|
||||
}
|
||||
}
|
||||
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[];
|
||||
export const BLACKLISTED_KEYS: string[];
|
||||
export namespace ACCOUNT_SETTINGS {
|
||||
const SUBSCRIPTIONS: string;
|
||||
const RECOMMENDED_VIDEOS: string;
|
||||
const CHANNEL_ACTIVITY: string;
|
||||
const COMMENT_REPLIES: string;
|
||||
const USER_MENTION: string;
|
||||
const SHARED_CONTENT: string;
|
||||
const PLAYLISTS_PRIVACY: string;
|
||||
const SUBSCRIPTIONS_PRIVACY: string;
|
||||
}
|
||||
export namespace BASE64_DIALECT {
|
||||
const NORMAL: string[];
|
||||
const REVERSE: string[];
|
||||
}
|
||||
export namespace SIG_REGEX {
|
||||
const ACTIONS: RegExp;
|
||||
const FUNCTIONS: RegExp;
|
||||
}
|
||||
export namespace NTOKEN_REGEX {
|
||||
export const CALLS: RegExp;
|
||||
export const PLACEHOLDERS: RegExp;
|
||||
const FUNCTIONS_1: RegExp;
|
||||
export { FUNCTIONS_1 as FUNCTIONS };
|
||||
}
|
||||
export namespace FUNCS {
|
||||
const PUSH: string;
|
||||
const REVERSE_1: string;
|
||||
const REVERSE_2: string;
|
||||
const SPLICE: string;
|
||||
const SWAP0_1: string;
|
||||
const SWAP0_2: string;
|
||||
const ROTATE_1: string;
|
||||
const ROTATE_2: string;
|
||||
const BASE64_DIA: string;
|
||||
const TRANSLATE_1: string;
|
||||
const TRANSLATE_2: string;
|
||||
}
|
||||
12
typings/lib/utils/Request.d.ts
vendored
Normal file
12
typings/lib/utils/Request.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
export = Request;
|
||||
/** @namespace */
|
||||
declare class Request {
|
||||
/**
|
||||
* @param {Innertube} session
|
||||
* @constructor
|
||||
*/
|
||||
constructor(session: Innertube);
|
||||
session: Innertube;
|
||||
instance: any;
|
||||
#private;
|
||||
}
|
||||
95
typings/lib/utils/Utils.d.ts
vendored
Normal file
95
typings/lib/utils/Utils.d.ts
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
/** @namespace */
|
||||
export class InnertubeError extends Error {
|
||||
constructor(message: any, info: any);
|
||||
info: any;
|
||||
date: Date;
|
||||
version: any;
|
||||
}
|
||||
export class UnavailableContentError extends InnertubeError {
|
||||
}
|
||||
export class ParsingError extends InnertubeError {
|
||||
}
|
||||
export class DownloadError extends InnertubeError {
|
||||
}
|
||||
export class MissingParamError extends InnertubeError {
|
||||
}
|
||||
export class NoStreamingDataError extends InnertubeError {
|
||||
}
|
||||
/**
|
||||
* Utility to help access deep properties of an object.
|
||||
*
|
||||
* @param {object} obj - the object.
|
||||
* @param {string} key - key of the property being accessed.
|
||||
* @param {string} target - anything that might be inside of the property.
|
||||
* @param {number} depth - maximum number of nested objects to flatten.
|
||||
* @param {boolean} safe - if set to true arrays will be preserved.
|
||||
*
|
||||
* @returns {object|Array.<any>}
|
||||
*/
|
||||
export function findNode(obj: object, key: string, target: string, depth: number, safe?: boolean): object | Array<any>;
|
||||
/**
|
||||
* Returns a random user agent.
|
||||
*
|
||||
* @param {string} type - mobile | desktop
|
||||
* @returns {object}
|
||||
*/
|
||||
export function getRandomUserAgent(type: string): object;
|
||||
/**
|
||||
* Generates an authentication token from a cookies' sid.
|
||||
*
|
||||
* @param {string} sid - Sid extracted from cookies
|
||||
* @returns {string}
|
||||
*/
|
||||
export function generateSidAuth(sid: string): string;
|
||||
/**
|
||||
* Generates a random string with a given length.
|
||||
*
|
||||
* @param {number} length
|
||||
* @returns {string}
|
||||
*/
|
||||
export function generateRandomString(length: number): string;
|
||||
/**
|
||||
* Finds a string between two delimiters.
|
||||
*
|
||||
* @param {string} data - the data.
|
||||
* @param {string} start_string - start string.
|
||||
* @param {string} end_string - end string.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getStringBetweenStrings(data: string, start_string: string, end_string: string): string;
|
||||
/**
|
||||
* Converts strings in camelCase to snake_case.
|
||||
*
|
||||
* @param {string} string The string in camelCase.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function camelToSnake(string: string): string;
|
||||
/**
|
||||
* Checks if a given client is valid.
|
||||
*
|
||||
* @param {string} client
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isValidClient(client: string): boolean;
|
||||
/**
|
||||
* Throws an error if given parameters are undefined.
|
||||
*
|
||||
* @param {object} params
|
||||
* @returns
|
||||
*/
|
||||
export function throwIfMissing(params: object): void;
|
||||
/**
|
||||
* Converts time (h:m:s) to seconds.
|
||||
*
|
||||
* @param {string} time
|
||||
* @returns {number} seconds
|
||||
*/
|
||||
export function timeToSeconds(time: string): number;
|
||||
/**
|
||||
* Turns the ntoken transform data into a valid json array
|
||||
*
|
||||
* @param {string} data
|
||||
* @returns {string}
|
||||
*/
|
||||
export function refineNTokenData(data: string): string;
|
||||
Reference in New Issue
Block a user