mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 17:42:18 +00:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aeff0c3fdc | ||
|
|
00d67ed417 | ||
|
|
78f93c7118 | ||
|
|
6db3f0ad91 | ||
|
|
cf48385f72 | ||
|
|
e70eab2416 | ||
|
|
771c6050c4 | ||
|
|
5670228a4f | ||
|
|
62ae384f27 | ||
|
|
185cdbd6ce | ||
|
|
5dd6ef9e24 | ||
|
|
309942090d | ||
|
|
af4a4b8b82 | ||
|
|
b9c9d40077 | ||
|
|
62fbc166c5 | ||
|
|
c3991dda32 | ||
|
|
95e804e8ea | ||
|
|
67a8435421 | ||
|
|
1847558d50 | ||
|
|
bde915bce3 | ||
|
|
a9ad3a31b5 | ||
|
|
e52e6138bd | ||
|
|
76248ad143 | ||
|
|
94f441a4e2 | ||
|
|
685e14fcc1 | ||
|
|
3cd115461f | ||
|
|
6da4ee8fd4 | ||
|
|
b095044baa | ||
|
|
ba2b757fdb | ||
|
|
9d7d0d83e1 | ||
|
|
b893e46634 | ||
|
|
d8ab6f3887 | ||
|
|
eea5ebfd04 | ||
|
|
b2117f11b9 |
3
.github/workflows/node.js.yml
vendored
3
.github/workflows/node.js.yml
vendored
@@ -1,6 +1,5 @@
|
||||
# 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
|
||||
# TODO: Make actual tests instead of just running the examples file
|
||||
|
||||
name: Build
|
||||
|
||||
@@ -27,4 +26,4 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install
|
||||
- run: node ./examples
|
||||
- run: npm test
|
||||
|
||||
26
README.md
26
README.md
@@ -16,7 +16,7 @@ As of now, this is one of the most advanced & stable YouTube libraries out there
|
||||
- Fetch notifications
|
||||
- Fetch subscriptions feed
|
||||
- Change notifications preferences for a channel
|
||||
- Subscribe/Unsubscribe/Like/Dislike/Comment
|
||||
- Subscribe/Unsubscribe/Like/Dislike/Comment, etc
|
||||
- Easily sign into your account in an easy & reliable way.
|
||||
- Last but not least, you can also download videos!
|
||||
|
||||
@@ -519,7 +519,9 @@ await video.comment('Haha, nice!');
|
||||
* Change notification preferences:
|
||||
```js
|
||||
const video = await youtube.getDetails(VIDEO_ID_HERE);
|
||||
await video.setNotificationPref('ALL'); // ALL | NONE | PERSONALIZED
|
||||
|
||||
// Can be: ALL | NONE | PERSONALIZED
|
||||
await video.setNotificationPref('ALL');
|
||||
```
|
||||
|
||||
All of the above interactions will return ```{ success: true, status_code: 200 }``` if everything goes alright.
|
||||
@@ -533,10 +535,10 @@ const Innertube = require('youtubei.js');
|
||||
async function start() {
|
||||
const youtube = await new Innertube();
|
||||
|
||||
const search = await youtube.search('Some random live');
|
||||
const search = await youtube.search('Some random livestream');
|
||||
const video = await youtube.getDetails(search.videos[0].id);
|
||||
|
||||
// This should only be called if you're sure it's a live and that it's still ongoing
|
||||
// This should only be called if you're sure it's a livestream and that it's still ongoing
|
||||
const livechat = video.getLivechat();
|
||||
|
||||
// Updated stats about the livestream
|
||||
@@ -563,7 +565,7 @@ livechat.stop();
|
||||
|
||||
Deleting a message:
|
||||
```js
|
||||
const msg = await livechat.sendMessage('Nice live!');
|
||||
const msg = await livechat.sendMessage('Nice livestream!');
|
||||
await msg.deleteMessage();
|
||||
```
|
||||
|
||||
@@ -581,7 +583,7 @@ async function start() {
|
||||
const search = await youtube.search('Looking for life on Mars - documentary');
|
||||
const stream = youtube.download(search.videos[0].id, {
|
||||
format: 'mp4', // Optional, ignored when type is set to audio and defaults to mp4, and I recommend to leave it as it is
|
||||
quality: '360p', // if a video doesn't have a specific quality it'll fall back to 360p, this is ignored when type is set to audio
|
||||
quality: '360p', // if a video doesn't have a specific quality it'll fall back to 360p, also ignored when type is set to audio
|
||||
type: 'videoandaudio' // can be “video”, “audio” and “videoandaudio”
|
||||
});
|
||||
|
||||
@@ -592,6 +594,7 @@ async function start() {
|
||||
});
|
||||
|
||||
stream.on('info', (info) => {
|
||||
// { video_details: {..}, selected_format: {..}, formats: {..} }
|
||||
console.info('[DOWNLOADER]', `Downloading ${info.video_details.title} by ${info.video_details.metadata.channel_name}`);
|
||||
});
|
||||
|
||||
@@ -613,6 +616,16 @@ async function start() {
|
||||
start();
|
||||
```
|
||||
|
||||
You can also download only a portion of a video by specifying a range:
|
||||
```js
|
||||
const stream = youtube.download(VIDEO_ID, {
|
||||
//...
|
||||
type: 'videoandaudio',
|
||||
range: { start: 0, end: 1048576 * 5 }
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
Cancelling a download:
|
||||
```js
|
||||
stream.cancel();
|
||||
@@ -681,6 +694,7 @@ Never sign-in with your personal account, you might get banned if you spam (don'
|
||||
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
|
||||
|
||||
Please make sure to update tests as appropriate.
|
||||
|
||||
## Disclaimer
|
||||
This project is not affiliated with, endorsed, or sponsored by YouTube or any of their affiliates or subsidiaries.
|
||||
All trademarks, logos and brand names are the property of their respective owners.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
const Uuid = require('uuid');
|
||||
const Axios = require('axios');
|
||||
const Utils = require('./Utils');
|
||||
const Constants = require('./Constants');
|
||||
const Uuid = require('uuid');
|
||||
|
||||
async function engage(session, engagement_type, args = {}) {
|
||||
if (!session.logged_in) throw new Error('You are not logged in');
|
||||
@@ -36,7 +36,7 @@ async function engage(session, engagement_type, args = {}) {
|
||||
default:
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/${engagement_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, id: args.video_id, data })).catch((error) => error);
|
||||
const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/${engagement_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session, id: args.video_id, data })).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
return {
|
||||
success: true,
|
||||
@@ -57,7 +57,7 @@ async function browse(session, action_type) {
|
||||
default:
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/browse${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session })).catch((error) => error);
|
||||
const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/browse${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session })).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
return {
|
||||
success: true,
|
||||
@@ -91,7 +91,7 @@ async function notifications(session, action_type, args = {}) {
|
||||
default:
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/notification/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session })).catch((error) => error);
|
||||
const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/notification/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session })).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
if (action_type === 'modify_channel_preference') return { success: true, status_code: response.status };
|
||||
return {
|
||||
@@ -108,7 +108,7 @@ async function livechat(session, action_type, args = {}) {
|
||||
data = {
|
||||
context: session.context,
|
||||
params: Utils.generateMessageParams(args.channel_id, args.video_id),
|
||||
clientMessageId: `INntLiB${Uuid.v4()}`,
|
||||
clientMessageId: `ytjs-${Uuid.v4()}`,
|
||||
richMessage: {
|
||||
textSegments: [{ text: args.text }]
|
||||
}
|
||||
@@ -128,7 +128,7 @@ async function livechat(session, action_type, args = {}) {
|
||||
default:
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, params: args.params })).catch((error) => error);
|
||||
const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session, params: args.params })).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
return {
|
||||
success: true,
|
||||
@@ -139,10 +139,7 @@ async function livechat(session, action_type, args = {}) {
|
||||
|
||||
async function getContinuation(session, info = {}) {
|
||||
let data = { context: session.context };
|
||||
|
||||
if (info.continuation_token) {
|
||||
data.continuation = info.continuation_token;
|
||||
}
|
||||
info.continuation_token && (data.continuation = info.continuation_token);
|
||||
|
||||
if (info.video_id) {
|
||||
data.videoId = info.video_id;
|
||||
@@ -156,7 +153,7 @@ async function getContinuation(session, info = {}) {
|
||||
data.captionsRequested = false;
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/next${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session })).catch((error) => error);
|
||||
const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/next${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session })).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
return {
|
||||
success: true,
|
||||
|
||||
581
lib/Constants.js
581
lib/Constants.js
@@ -2,334 +2,265 @@
|
||||
|
||||
const Utils = require('./Utils');
|
||||
|
||||
const urls = {
|
||||
YT_BASE_URL: 'https://www.youtube.com',
|
||||
YT_MOBILE_URL: 'https://m.youtube.com',
|
||||
YT_WATCH_PAGE: 'https://m.youtube.com/watch',
|
||||
};
|
||||
|
||||
const oauth = {
|
||||
scope: 'http://gdata.youtube.com https://www.googleapis.com/auth/youtube-paid-content',
|
||||
grant_type: 'http://oauth.net/grant_type/device/1.0',
|
||||
model_name: 'ytlr::'
|
||||
};
|
||||
|
||||
const oauth_reqopts = {
|
||||
headers: {
|
||||
'accept': '*/*',
|
||||
'origin': urls.YT_BASE_URL,
|
||||
'user-agent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version',
|
||||
'content-type': 'application/json',
|
||||
'x-requested-with': 'mark.via.gp',
|
||||
'referer': `${urls.YT_BASE_URL}/tv`,
|
||||
'accept-language': 'en-US'
|
||||
}
|
||||
};
|
||||
|
||||
const default_headers = (session) => {
|
||||
return {
|
||||
headers: {
|
||||
'Cookie': session.cookie,
|
||||
'user-agent': Utils.getRandomUserAgent('desktop').userAgent,
|
||||
'Referer': 'https://www.google.com/',
|
||||
'Accept': 'text/html',
|
||||
'Accept-Language': 'en-US,en',
|
||||
'Accept-Encoding': 'gzip',
|
||||
'Upgrade-Insecure-Requests': 1
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const innertube_request_opts = (info) => {
|
||||
if (info.desktop === undefined) info.desktop = true;
|
||||
let req_opts = {
|
||||
params: info.params || {},
|
||||
headers: {
|
||||
'accept': '*/*',
|
||||
'user-agent': Utils.getRandomUserAgent(info.desktop ? 'desktop' : 'mobile').userAgent,
|
||||
'content-type': 'application/json',
|
||||
'accept-language': 'en-US,en;q=0.9',
|
||||
'x-goog-authuser': 0,
|
||||
'x-goog-visitor-id': info.session.context.client.visitorData,
|
||||
'x-youtube-client-name': info.desktop ? 1 : 2,
|
||||
'x-youtube-client-version': info.session.context.client.clientVersion,
|
||||
'x-youtube-chrome-connected': 'source=Chrome,mode=0,enable_account_consistency=true,supervised=false,consistency_enabled_by_default=false',
|
||||
'x-origin': info.desktop ? urls.YT_BASE_URL : urls.YT_MOBILE_URL,
|
||||
'origin': info.desktop ? urls.YT_BASE_URL : urls.YT_MOBILE_URL,
|
||||
}
|
||||
};
|
||||
|
||||
if (info.session.logged_in && info.desktop) {
|
||||
req_opts.headers.Cookie = info.session.cookie;
|
||||
req_opts.headers.authorization = info.session.cookie.length < 1 ? `Bearer ${info.session.access_token}` : info.session.auth_apisid;
|
||||
}
|
||||
|
||||
if (info.id) {
|
||||
req_opts.headers.referer = (info.desktop ? urls.YT_BASE_URL : urls.YT_MOBILE_URL) + '/watch?v=' + info.id;
|
||||
}
|
||||
|
||||
return req_opts;
|
||||
};
|
||||
|
||||
const video_details_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': urls.YT_BASE_URL,
|
||||
'lactMilliseconds': '-1'
|
||||
module.exports = {
|
||||
URLS: {
|
||||
YT_BASE_URL: 'https://www.youtube.com',
|
||||
YT_MOBILE_URL: 'https://m.youtube.com',
|
||||
YT_WATCH_PAGE: 'https://m.youtube.com/watch'
|
||||
},
|
||||
OAUTH: {
|
||||
SCOPE: 'http://gdata.youtube.com https://www.googleapis.com/auth/youtube-paid-content',
|
||||
GRANT_TYPE: 'http://oauth.net/grant_type/device/1.0',
|
||||
MODEL_NAME: 'ytlr::',
|
||||
HEADERS: {
|
||||
headers: {
|
||||
'accept': '*/*',
|
||||
'origin': 'https://www.youtube.com',
|
||||
'user-agent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version',
|
||||
'content-type': 'application/json',
|
||||
'referer': `https://www.youtube.com/tv`,
|
||||
'accept-language': 'en-US'
|
||||
}
|
||||
},
|
||||
context: context,
|
||||
videoId: id
|
||||
};
|
||||
};
|
||||
|
||||
const stream_headers = (range) => {
|
||||
let headers = {
|
||||
}
|
||||
},
|
||||
DEFAULT_HEADERS: (session) => {
|
||||
return {
|
||||
headers: {
|
||||
'Cookie': session.cookie,
|
||||
'user-agent': Utils.getRandomUserAgent('desktop').userAgent,
|
||||
'Referer': 'https://www.google.com/',
|
||||
'Accept': 'text/html',
|
||||
'Accept-Language': 'en-US,en',
|
||||
'Accept-Encoding': 'gzip',
|
||||
'Upgrade-Insecure-Requests': 1
|
||||
}
|
||||
};
|
||||
},
|
||||
STREAM_HEADERS: {
|
||||
'Accept': '*/*',
|
||||
'User-Agent': Utils.getRandomUserAgent('desktop').userAgent,
|
||||
'Connection': 'keep-alive',
|
||||
'Origin': urls.YT_BASE_URL,
|
||||
'Referer': urls.YT_BASE_URL,
|
||||
'Origin': 'https://www.youtube.com',
|
||||
'Referer': 'https://www.youtube.com',
|
||||
'DNT': '?1'
|
||||
};
|
||||
if (range) {
|
||||
headers.Range = range;
|
||||
},
|
||||
INNERTUBE_REQOPTS: (info) => {
|
||||
info.desktop === undefined && (info.desktop = true);
|
||||
let req_opts = {
|
||||
params: info.params || {},
|
||||
headers: {
|
||||
'accept': '*/*',
|
||||
'user-agent': Utils.getRandomUserAgent(info.desktop ? 'desktop' : 'mobile').userAgent,
|
||||
'content-type': 'application/json',
|
||||
'accept-language': 'en-US,en;q=0.9',
|
||||
'x-goog-authuser': 0,
|
||||
'x-goog-visitor-id': info.session.context.client.visitorData || '',
|
||||
'x-youtube-client-name': info.desktop ? 1 : 2,
|
||||
'x-youtube-client-version': info.session.context.client.clientVersion,
|
||||
'x-youtube-chrome-connected': 'source=Chrome,mode=0,enable_account_consistency=true,supervised=false,consistency_enabled_by_default=false',
|
||||
'x-origin': info.desktop ? 'https://www.youtube.com' : 'https://m.youtube.com',
|
||||
'origin': info.desktop ? 'https://www.youtube.com' : 'https://m.youtube.com',
|
||||
}
|
||||
};
|
||||
|
||||
info.id && (req_opts.headers.referer = (info.desktop ? 'https://www.youtube.com' : 'https://m.youtube.com') + '/watch?v=' + info.id);
|
||||
|
||||
if (info.session.logged_in && info.desktop) {
|
||||
req_opts.headers.Cookie = info.session.cookie;
|
||||
req_opts.headers.authorization = info.session.cookie.length < 1 ? `Bearer ${info.session.access_token}` : info.session.auth_apisid;
|
||||
}
|
||||
|
||||
return req_opts;
|
||||
},
|
||||
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
|
||||
};
|
||||
},
|
||||
BASE64_DIALECT: {
|
||||
NORMAL: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'.split(''),
|
||||
REVERSE: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'.split('')
|
||||
},
|
||||
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 h=f|function\(d\){for\(var|reverse\(\)\.forEach|unshift\(d\.pop\(\)\)|function\(d,e\){for\(var f/,
|
||||
FUNCS: {
|
||||
PUSH: 'd.push(e)',
|
||||
REVERSE_1: 'd.reverse()',
|
||||
REVERSE_2: 'function(d){for(var',
|
||||
SPLICE: 'd.length;d.splice(e,1)',
|
||||
SWAP0_1: 'd[0])[0])',
|
||||
SWAP0_2: 'f=d[0];d[0]',
|
||||
ROTATE_1: 'reverse().forEach',
|
||||
ROTATE_2: 'unshift(d.pop())',
|
||||
BASE64_DIA: 'function(){for(var',
|
||||
TRANSLATE_1: 'function(d,e){for(var f',
|
||||
TRANSLATE_2: 'function(d,e,f){var h=f'
|
||||
},
|
||||
// Helper functions, felt like Utils.js wasn't the right place for them:
|
||||
formatNTransformData: (data) => {
|
||||
return data
|
||||
.replace(/function\(d,e\)/g, '"function(d,e)').replace(/function\(d\)/g, '"function(d)')
|
||||
.replace(/function\(\)/g, '"function()').replace(/function\(d,e,f\)/g, '"function(d,e,f)')
|
||||
.replace(/\[function\(d,e,f\)/g, '["function(d,e,f)')
|
||||
.replace(/,b,/g, ',"b",').replace(/,b/g, ',"b"').replace(/b,/g, '"b",').replace(/b]/g, '"b"]')
|
||||
.replace(/\[b/g, '["b"').replace(/}]/g, '"]').replace(/},/g, '}",').replace(/""/g, '')
|
||||
.replace(/length]\)}"/g, 'length])}');
|
||||
},
|
||||
formatVideoData: (data, context, is_desktop) => {
|
||||
let video_details = {};
|
||||
let metadata = {};
|
||||
|
||||
if (is_desktop) {
|
||||
metadata.embed = data.microformat.playerMicroformatRenderer.embed;
|
||||
metadata.view_count = parseInt(data.videoDetails.viewCount);
|
||||
metadata.average_rating = data.videoDetails.averageRating;
|
||||
metadata.length_seconds = data.microformat.playerMicroformatRenderer.lengthSeconds;
|
||||
metadata.channel_id = data.videoDetails.channelId;
|
||||
metadata.channel_url = data.microformat.playerMicroformatRenderer.ownerProfileUrl;
|
||||
metadata.external_channel_id = data.microformat.playerMicroformatRenderer.externalChannelId;
|
||||
metadata.is_live_content = data.videoDetails.isLiveContent;
|
||||
metadata.is_family_safe = data.microformat.playerMicroformatRenderer.isFamilySafe;
|
||||
metadata.is_unlisted = data.microformat.playerMicroformatRenderer.isUnlisted;
|
||||
metadata.is_private = data.videoDetails.isPrivate;
|
||||
metadata.has_ypc_metadata = data.microformat.playerMicroformatRenderer.hasYpcMetadata;
|
||||
metadata.category = data.microformat.playerMicroformatRenderer.category;
|
||||
metadata.channel_name = data.microformat.playerMicroformatRenderer.ownerChannelName;
|
||||
metadata.publish_date = data.microformat.playerMicroformatRenderer.publishDate || 'N/A';
|
||||
metadata.upload_date = data.microformat.playerMicroformatRenderer.uploadDate || 'N/A';
|
||||
metadata.keywords = data.videoDetails.keywords || [];
|
||||
|
||||
video_details.title = data.videoDetails.title;
|
||||
video_details.description = data.videoDetails.shortDescription;
|
||||
video_details.thumbnail = data.videoDetails.thumbnail.thumbnails.slice(-1)[0];
|
||||
video_details.metadata = metadata;
|
||||
} else {
|
||||
metadata.embed = data[2].playerResponse.microformat.playerMicroformatRenderer.embed;
|
||||
metadata.likes = parseInt(data[3].response.contents.singleColumnWatchNextResults.results.results.contents[1].slimVideoMetadataSectionRenderer.contents[1].slimVideoActionBarRenderer.buttons[0].slimMetadataToggleButtonRenderer.button.toggleButtonRenderer.defaultText.accessibility.accessibilityData.label.replace(/\D/g, ''));
|
||||
metadata.dislikes = parseInt(data[3].response.contents.singleColumnWatchNextResults.results.results.contents[1].slimVideoMetadataSectionRenderer.contents[1].slimVideoActionBarRenderer.buttons[1].slimMetadataToggleButtonRenderer.button.toggleButtonRenderer.defaultText.accessibility.accessibilityData.label.replace(/\D/g, ''));
|
||||
metadata.view_count = parseInt(data[2].playerResponse.videoDetails.viewCount);
|
||||
metadata.average_rating = data[2].playerResponse.videoDetails.averageRating;
|
||||
metadata.length_seconds = data[2].playerResponse.microformat.playerMicroformatRenderer.lengthSeconds;
|
||||
metadata.channel_id = data[2].playerResponse.videoDetails.channelId;
|
||||
metadata.channel_url = data[2].playerResponse.microformat.playerMicroformatRenderer.ownerProfileUrl;
|
||||
metadata.external_channel_id = data[2].playerResponse.microformat.playerMicroformatRenderer.externalChannelId;
|
||||
metadata.is_live_content = data[2].playerResponse.videoDetails.isLiveContent;
|
||||
metadata.is_family_safe = data[2].playerResponse.microformat.playerMicroformatRenderer.isFamilySafe;
|
||||
metadata.is_unlisted = data[2].playerResponse.microformat.playerMicroformatRenderer.isUnlisted;
|
||||
metadata.is_private = data[2].playerResponse.videoDetails.isPrivate;
|
||||
metadata.has_ypc_metadata = data[2].playerResponse.microformat.playerMicroformatRenderer.hasYpcMetadata;
|
||||
metadata.category = data[2].playerResponse.microformat.playerMicroformatRenderer.category;
|
||||
metadata.channel_name = data[2].playerResponse.microformat.playerMicroformatRenderer.ownerChannelName;
|
||||
metadata.publish_date = data[2].playerResponse.microformat.playerMicroformatRenderer.publishDate;
|
||||
metadata.upload_date = data[2].playerResponse.microformat.playerMicroformatRenderer.uploadDate;
|
||||
metadata.keywords = data[2].playerResponse.videoDetails.keywords;
|
||||
|
||||
video_details.title = data[2].playerResponse.videoDetails.title;
|
||||
video_details.description = data[2].playerResponse.videoDetails.shortDescription;
|
||||
video_details.thumbnail = data[2].playerResponse.videoDetails.thumbnail.thumbnails.slice(-1)[0];
|
||||
|
||||
// Functions
|
||||
video_details.like = () => {};
|
||||
video_details.dislike = () => {};
|
||||
video_details.removeLike = () => {};
|
||||
video_details.subscribe = () => {};
|
||||
video_details.unsubscribe = () => {};
|
||||
video_details.comment = () => {};
|
||||
video_details.getComments = () => {};
|
||||
video_details.setNotificationPref = () => {};
|
||||
video_details.getLivechat = () => {};
|
||||
|
||||
// Additional metadata
|
||||
video_details.metadata = metadata;
|
||||
}
|
||||
return video_details;
|
||||
},
|
||||
filters: (order) => {
|
||||
return (({
|
||||
'any,any,relevance': 'EgIQAQ%3D%3D',
|
||||
'hour,any,relevance': 'EgIIAQ%3D%3D',
|
||||
'day,any,relevance': 'EgQIAhAB',
|
||||
'week,any,relevance': 'EgQIAxAB',
|
||||
'month,any,relevance': 'EgQIBBAB',
|
||||
'year,any,relevance': 'EgQIBRAB',
|
||||
'any,short,relevance': 'EgQQARgB',
|
||||
'hour,short,relevance': 'EgYIARABGAE%3D',
|
||||
'day,short,relevance': 'EgYIAhABGAE%3D',
|
||||
'week,short,relevance': 'EgYIAxABGAE%3D',
|
||||
'month,short,relevance': 'EgYIBBABGAE%3D',
|
||||
'year,short,relevance': 'EgYIBRABGAE%3D',
|
||||
'any,long,relevance': 'EgQQARgC',
|
||||
'hour,long,relevance': 'EgYIARABGAI%3D',
|
||||
'day,long,relevance': 'EgYIAhABGAI%3D',
|
||||
'week,long,relevance': 'EgYIAxABGAI%3D',
|
||||
'month,long,relevance': 'EgYIBBABGAI%3D',
|
||||
'year,long,relevance': 'EgYIBRABGAI%3D',
|
||||
'any,any,age': 'CAISAhAB',
|
||||
'hour,any,age': 'CAISBAgBEAE%3D',
|
||||
'day,any,age': 'CAISBAgCEAE%3D',
|
||||
'week,any,age': 'CAISBAgDEAE%3D',
|
||||
'month,any,age': 'CAISBAgEEAE%3D',
|
||||
'year,any,age': 'CAISBAgFEAE%3D',
|
||||
'any,short,age': 'CAISBBABGAE%3D',
|
||||
'hour,short,age': 'CAISBggBEAEYAQ%3D%3D',
|
||||
'day,short,age': 'CAISBggCEAEYAQ%3D%3D',
|
||||
'week,short,age': 'CAISBggDEAEYAQ%3D%3D',
|
||||
'month,short,age': 'CAISBggEEAEYAQ%3D%3D',
|
||||
'year,short,age': 'CAISBggFEAEYAQ%3D%3D',
|
||||
'any,long,age': 'CAISBBABGAI%3D',
|
||||
'hour,long,age': 'CAISBggBEAEYAg%3D%3D',
|
||||
'day,long,age': 'CAISBggCEAEYAg%3D%3D',
|
||||
'week,long,age': 'CAISBggDEAEYAg%3D%3D',
|
||||
'month,long,age': 'CAISBggEEAEYAg%3D%3D',
|
||||
'year,long,age': 'CAISBggFEAEYAg%3D%3D',
|
||||
'any,any,views': 'CAMSAhAB',
|
||||
'hour,any,views': 'CAMSBAgBEAE%3D',
|
||||
'day,any,views': 'CAMSBAgCEAE%3D',
|
||||
'week,any,views': 'CAMSBAgDEAE%3D',
|
||||
'month,any,views': 'CAMSBAgEEAE%3D',
|
||||
'year,any,views': 'CAMSBAgFEAE%3D',
|
||||
'any,short,views': 'CAMSBBABGAE%3D',
|
||||
'hour,short,views': 'CAMSBggBEAEYAQ%3D%3D',
|
||||
'day,short,views': 'CAMSBggCEAEYAQ%3D%3D',
|
||||
'week,short,views': 'CAMSBggDEAEYAQ%3D%3D',
|
||||
'month,short,views': 'CAMSBggEEAEYAQ%3D%3D',
|
||||
'year,short,views': 'CAMSBggFEAEYAQ%3D%3D',
|
||||
'any,long,views': 'CAMSBBABGAI%3D',
|
||||
'hour,long,views': 'CAMSBggBEAEYAg%3D%3D',
|
||||
'day,long,views': 'CAMSBggCEAEYAg%3D%3D',
|
||||
'week,long,views': 'CAMSBggDEAEYAg%3D%3D',
|
||||
'month,long,views': 'CAMSBggEEAEYAg%3D%3D',
|
||||
'year,long,views': 'CAMSBggFEAEYAg%3D%3D',
|
||||
'any,any,rating': 'CAESAhAB',
|
||||
'hour,any,rating': 'CAESBAgBEAE%3D',
|
||||
'day,any,rating': 'CAESBAgCEAE%3D',
|
||||
'week,any,rating': 'CAESBAgDEAE%3D',
|
||||
'month,any,rating': 'CAESBAgEEAE%3D',
|
||||
'year,any,rating': 'CAESBAgFEAE%3D',
|
||||
'any,short,rating': 'CAESBBABGAE%3D',
|
||||
'hour,short,rating': 'CAESBggBEAEYAQ%3D%3D',
|
||||
'day,short,rating': 'CAESBggCEAEYAQ%3D%3D',
|
||||
'week,short,rating': 'CAESBggDEAEYAQ%3D%3D',
|
||||
'month,short,rating': 'CAESBggEEAEYAQ%3D%3D',
|
||||
'year,short,rating': 'CAESBggFEAEYAQ%3D%3D',
|
||||
'any,long,rating': 'CAESBBABGAI%3D',
|
||||
'hour,long,rating': 'CAESBggBEAEYAg%3D%3D',
|
||||
'day,long,rating': 'CAESBggCEAEYAg%3D%3D',
|
||||
'week,long,rating': 'CAESBggDEAEYAg%3D%3D',
|
||||
'month,long,rating': 'CAESBggEEAEYAg%3D%3D',
|
||||
'year,long,rating': 'CAESBggFEAEYAg%3D%3D'
|
||||
})[order] || 'EgIQAQ%3D%3D');
|
||||
}
|
||||
return headers;
|
||||
};
|
||||
|
||||
const formatVideoData = (data, context, desktop) => {
|
||||
let video_details = {};
|
||||
let metadata = {};
|
||||
|
||||
if (desktop) {
|
||||
metadata.embed = data.microformat.playerMicroformatRenderer.embed;
|
||||
metadata.view_count = parseInt(data.videoDetails.viewCount);
|
||||
metadata.average_rating = data.videoDetails.averageRating;
|
||||
metadata.length_seconds = data.microformat.playerMicroformatRenderer.lengthSeconds;
|
||||
metadata.channel_id = data.videoDetails.channelId;
|
||||
metadata.channel_url = data.microformat.playerMicroformatRenderer.ownerProfileUrl;
|
||||
metadata.external_channel_id = data.microformat.playerMicroformatRenderer.externalChannelId;
|
||||
metadata.is_live_content = data.videoDetails.isLiveContent;
|
||||
metadata.is_family_safe = data.microformat.playerMicroformatRenderer.isFamilySafe;
|
||||
metadata.is_unlisted = data.microformat.playerMicroformatRenderer.isUnlisted;
|
||||
metadata.is_private = data.videoDetails.isPrivate;
|
||||
metadata.has_ypc_metadata = data.microformat.playerMicroformatRenderer.hasYpcMetadata;
|
||||
metadata.category = data.microformat.playerMicroformatRenderer.category;
|
||||
metadata.channel_name = data.microformat.playerMicroformatRenderer.ownerChannelName;
|
||||
metadata.publish_date = data.microformat.playerMicroformatRenderer.publishDate || 'N/A';
|
||||
metadata.upload_date = data.microformat.playerMicroformatRenderer.uploadDate || 'N/A';
|
||||
metadata.keywords = data.videoDetails.keywords || [];
|
||||
|
||||
video_details.title = data.videoDetails.title;
|
||||
video_details.description = data.videoDetails.shortDescription;
|
||||
video_details.thumbnail = data.videoDetails.thumbnail.thumbnails.slice(-1)[0];
|
||||
video_details.metadata = metadata;
|
||||
} else {
|
||||
metadata.embed = data[2].playerResponse.microformat.playerMicroformatRenderer.embed;
|
||||
metadata.likes = parseInt(data[3].response.contents.singleColumnWatchNextResults.results.results.contents[1].slimVideoMetadataSectionRenderer.contents[1].slimVideoActionBarRenderer.buttons[0].slimMetadataToggleButtonRenderer.button.toggleButtonRenderer.defaultText.accessibility.accessibilityData.label.replace(/\D/g, ''));
|
||||
metadata.dislikes = parseInt(data[3].response.contents.singleColumnWatchNextResults.results.results.contents[1].slimVideoMetadataSectionRenderer.contents[1].slimVideoActionBarRenderer.buttons[1].slimMetadataToggleButtonRenderer.button.toggleButtonRenderer.defaultText.accessibility.accessibilityData.label.replace(/\D/g, ''));
|
||||
metadata.view_count = parseInt(data[2].playerResponse.videoDetails.viewCount);
|
||||
metadata.average_rating = data[2].playerResponse.videoDetails.averageRating;
|
||||
metadata.length_seconds = data[2].playerResponse.microformat.playerMicroformatRenderer.lengthSeconds;
|
||||
metadata.channel_id = data[2].playerResponse.videoDetails.channelId;
|
||||
metadata.channel_url = data[2].playerResponse.microformat.playerMicroformatRenderer.ownerProfileUrl;
|
||||
metadata.external_channel_id = data[2].playerResponse.microformat.playerMicroformatRenderer.externalChannelId;
|
||||
metadata.is_live_content = data[2].playerResponse.videoDetails.isLiveContent;
|
||||
metadata.is_family_safe = data[2].playerResponse.microformat.playerMicroformatRenderer.isFamilySafe;
|
||||
metadata.is_unlisted = data[2].playerResponse.microformat.playerMicroformatRenderer.isUnlisted;
|
||||
metadata.is_private = data[2].playerResponse.videoDetails.isPrivate;
|
||||
metadata.has_ypc_metadata = data[2].playerResponse.microformat.playerMicroformatRenderer.hasYpcMetadata;
|
||||
metadata.category = data[2].playerResponse.microformat.playerMicroformatRenderer.category;
|
||||
metadata.channel_name = data[2].playerResponse.microformat.playerMicroformatRenderer.ownerChannelName;
|
||||
metadata.publish_date = data[2].playerResponse.microformat.playerMicroformatRenderer.publishDate;
|
||||
metadata.upload_date = data[2].playerResponse.microformat.playerMicroformatRenderer.uploadDate;
|
||||
metadata.keywords = data[2].playerResponse.videoDetails.keywords;
|
||||
|
||||
video_details.title = data[2].playerResponse.videoDetails.title;
|
||||
video_details.description = data[2].playerResponse.videoDetails.shortDescription;
|
||||
video_details.thumbnail = data[2].playerResponse.videoDetails.thumbnail.thumbnails.slice(-1)[0];
|
||||
|
||||
// Functions
|
||||
video_details.like = () => {};
|
||||
video_details.dislike = () => {};
|
||||
video_details.removeLike = () => {};
|
||||
video_details.subscribe = () => {};
|
||||
video_details.unsubscribe = () => {};
|
||||
video_details.comment = () => {};
|
||||
video_details.getComments = () => {};
|
||||
video_details.setNotificationPref = () => {};
|
||||
video_details.getLivechat = () => {};
|
||||
|
||||
// Additional metadata
|
||||
video_details.metadata = metadata;
|
||||
}
|
||||
return video_details;
|
||||
};
|
||||
|
||||
|
||||
const base64_alphabet = {
|
||||
normal: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'.split(''),
|
||||
reverse: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'.split('')
|
||||
};
|
||||
|
||||
const filters = (order) => {
|
||||
// TODO: Refactor this with protobuf encoding
|
||||
switch (order) {
|
||||
case 'any,any,relevance':
|
||||
return 'EgIQAQ%3D%3D';
|
||||
case 'hour,any,relevance':
|
||||
return 'EgIIAQ%3D%3D';
|
||||
case 'day,any,relevance':
|
||||
return 'EgQIAhAB';
|
||||
case 'week,any,relevance':
|
||||
return 'EgQIAxAB';
|
||||
case 'month,any,relevance':
|
||||
return 'EgQIBBAB';
|
||||
case 'year,any,relevance':
|
||||
return 'EgQIBRAB';
|
||||
case 'any,short,relevance':
|
||||
return 'EgQQARgB';
|
||||
case 'hour,short,relevance':
|
||||
return 'EgYIARABGAE%3D';
|
||||
case 'day,short,relevance':
|
||||
return 'EgYIAhABGAE%3D';
|
||||
case 'week,short,relevance':
|
||||
return 'EgYIAxABGAE%3D';
|
||||
case 'month,short,relevance':
|
||||
return 'EgYIBBABGAE%3D';
|
||||
case 'year,short,relevance':
|
||||
return 'EgYIBRABGAE%3D';
|
||||
case 'any,long,relevance':
|
||||
return 'EgQQARgC';
|
||||
case 'hour,long,relevance':
|
||||
return 'EgYIARABGAI%3D';
|
||||
case 'day,long,relevance':
|
||||
return 'EgYIAhABGAI%3D';
|
||||
case 'week,long,relevance':
|
||||
return 'EgYIAxABGAI%3D';
|
||||
case 'month,long,relevance':
|
||||
return 'EgYIBBABGAI%3D';
|
||||
case 'year,long,relevance':
|
||||
return 'EgYIBRABGAI%3D';
|
||||
case 'any,any,age':
|
||||
return 'CAISAhAB';
|
||||
case 'hour,any,age':
|
||||
return 'CAISBAgBEAE%3D';
|
||||
case 'day,any,age':
|
||||
return 'CAISBAgCEAE%3D';
|
||||
case 'week,any,age':
|
||||
return 'CAISBAgDEAE%3D';
|
||||
case 'month,any,age':
|
||||
return 'CAISBAgEEAE%3D';
|
||||
case 'year,any,age':
|
||||
return 'CAISBAgFEAE%3D';
|
||||
case 'any,short,age':
|
||||
return 'CAISBBABGAE%3D';
|
||||
case 'hour,short,age':
|
||||
return 'CAISBggBEAEYAQ%3D%3D';
|
||||
case 'day,short,age':
|
||||
return 'CAISBggCEAEYAQ%3D%3D';
|
||||
case 'week,short,age':
|
||||
return 'CAISBggDEAEYAQ%3D%3D';
|
||||
case 'month,short,age':
|
||||
return 'CAISBggEEAEYAQ%3D%3D';
|
||||
case 'year,short,age':
|
||||
return 'CAISBggFEAEYAQ%3D%3D';
|
||||
case 'any,long,age':
|
||||
return 'CAISBBABGAI%3D';
|
||||
case 'hour,long,age':
|
||||
return 'CAISBggBEAEYAg%3D%3D';
|
||||
case 'day,long,age':
|
||||
return 'CAISBggCEAEYAg%3D%3D';
|
||||
case 'week,long,age':
|
||||
return 'CAISBggDEAEYAg%3D%3D';
|
||||
case 'month,long,age':
|
||||
return 'CAISBggEEAEYAg%3D%3D';
|
||||
case 'year,long,age':
|
||||
return 'CAISBggFEAEYAg%3D%3D';
|
||||
case 'any,any,views':
|
||||
return 'CAMSAhAB';
|
||||
case 'hour,any,views':
|
||||
return 'CAMSBAgBEAE%3D';
|
||||
case 'day,any,views':
|
||||
return 'CAMSBAgCEAE%3D';
|
||||
case 'week,any,views':
|
||||
return 'CAMSBAgDEAE%3D';
|
||||
case 'month,any,views':
|
||||
return 'CAMSBAgEEAE%3D';
|
||||
case 'year,any,views':
|
||||
return 'CAMSBAgFEAE%3D';
|
||||
case 'any,short,views':
|
||||
return 'CAMSBBABGAE%3D';
|
||||
case 'hour,short,views':
|
||||
return 'CAMSBggBEAEYAQ%3D%3D';
|
||||
case 'day,short,views':
|
||||
return 'CAMSBggCEAEYAQ%3D%3D';
|
||||
case 'week,short,views':
|
||||
return 'CAMSBggDEAEYAQ%3D%3D';
|
||||
case 'month,short,views':
|
||||
return 'CAMSBggEEAEYAQ%3D%3D';
|
||||
case 'year,short,views':
|
||||
return 'CAMSBggFEAEYAQ%3D%3D';
|
||||
case 'any,long,views':
|
||||
return 'CAMSBBABGAI%3D';
|
||||
case 'hour,long,views':
|
||||
return 'CAMSBggBEAEYAg%3D%3D';
|
||||
case 'day,long,views':
|
||||
return 'CAMSBggCEAEYAg%3D%3D';
|
||||
case 'week,long,views':
|
||||
return 'CAMSBggDEAEYAg%3D%3D';
|
||||
case 'month,long,views':
|
||||
return 'CAMSBggEEAEYAg%3D%3D';
|
||||
case 'year,long,views':
|
||||
return 'CAMSBggFEAEYAg%3D%3D';
|
||||
case 'any,any,rating':
|
||||
return 'CAESAhAB';
|
||||
case 'hour,any,rating':
|
||||
return 'CAESBAgBEAE%3D';
|
||||
case 'day,any,rating':
|
||||
return 'CAESBAgCEAE%3D';
|
||||
case 'week,any,rating':
|
||||
return 'CAESBAgDEAE%3D';
|
||||
case 'month,any,rating':
|
||||
return 'CAESBAgEEAE%3D';
|
||||
case 'year,any,rating':
|
||||
return 'CAESBAgFEAE%3D';
|
||||
case 'any,short,rating':
|
||||
return 'CAESBBABGAE%3D';
|
||||
case 'hour,short,rating':
|
||||
return 'CAESBggBEAEYAQ%3D%3D';
|
||||
case 'day,short,rating':
|
||||
return 'CAESBggCEAEYAQ%3D%3D';
|
||||
case 'week,short,rating':
|
||||
return 'CAESBggDEAEYAQ%3D%3D';
|
||||
case 'month,short,rating':
|
||||
return 'CAESBggEEAEYAQ%3D%3D';
|
||||
case 'year,short,rating':
|
||||
return 'CAESBggFEAEYAQ%3D%3D';
|
||||
case 'any,long,rating':
|
||||
return 'CAESBBABGAI%3D';
|
||||
case 'hour,long,rating':
|
||||
return 'CAESBggBEAEYAg%3D%3D';
|
||||
case 'day,long,rating':
|
||||
return 'CAESBggCEAEYAg%3D%3D';
|
||||
case 'week,long,rating':
|
||||
return 'CAESBggDEAEYAg%3D%3D';
|
||||
case 'month,long,rating':
|
||||
return 'CAESBggEEAEYAg%3D%3D';
|
||||
case 'year,long,rating':
|
||||
return 'CAESBggFEAEYAg%3D%3D';
|
||||
default:
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { urls, oauth, oauth_reqopts, default_headers, innertube_request_opts, video_details_reqbody, stream_headers, formatVideoData, base64_alphabet, filters };
|
||||
};
|
||||
132
lib/Innertube.js
132
lib/Innertube.js
@@ -9,7 +9,7 @@ const NToken = require('./NToken');
|
||||
const Actions = require('./Actions');
|
||||
const Livechat = require('./Livechat');
|
||||
const Constants = require('./Constants');
|
||||
const SigDecipher = require('./SigDecipher');
|
||||
const SigDecipher = require('./Sig');
|
||||
const EventEmitter = require('events');
|
||||
const TimeToSeconds = require('time-to-seconds');
|
||||
const CancelToken = Axios.CancelToken;
|
||||
@@ -18,35 +18,43 @@ class Innertube extends EventEmitter {
|
||||
constructor(cookie) {
|
||||
super();
|
||||
this.cookie = cookie || '';
|
||||
this.retry_count = 0;
|
||||
return this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
const response = await Axios.get(Constants.urls.YT_BASE_URL, Constants.default_headers(this)).catch((error) => error);
|
||||
if (response instanceof Error) throw new Error('Could not retrieve Innertube configuration data: ' + response.message);
|
||||
let innertube_data = JSON.parse('{' + Utils.getStringBetweenStrings(response.data, 'ytcfg.set({', '});') + '}');
|
||||
if (innertube_data.INNERTUBE_CONTEXT) {
|
||||
this.context = innertube_data.INNERTUBE_CONTEXT;
|
||||
this.key = innertube_data.INNERTUBE_API_KEY;
|
||||
this.id_token = innertube_data.ID_TOKEN;
|
||||
this.session_token = innertube_data.XSRF_TOKEN;
|
||||
this.player_url = innertube_data.PLAYER_JS_URL;
|
||||
this.logged_in = innertube_data.LOGGED_IN;
|
||||
this.sts = innertube_data.STS;
|
||||
this.context.client.hl = 'en';
|
||||
this.context.client.gl = 'US';
|
||||
const response = await Axios.get(Constants.URLS.YT_BASE_URL, Constants.DEFAULT_HEADERS(this)).catch((error) => error);
|
||||
if (response instanceof Error) throw new Error(`Could not extract Innertube data: ${response.message}`);
|
||||
|
||||
this.player = new Player(this);
|
||||
await this.player.init();
|
||||
try {
|
||||
const innertube_data = JSON.parse(`{${Utils.getStringBetweenStrings(response.data, 'ytcfg.set({', '});')}}`);
|
||||
if (innertube_data.INNERTUBE_CONTEXT) {
|
||||
this.context = innertube_data.INNERTUBE_CONTEXT;
|
||||
this.key = innertube_data.INNERTUBE_API_KEY;
|
||||
this.id_token = innertube_data.ID_TOKEN;
|
||||
this.session_token = innertube_data.XSRF_TOKEN;
|
||||
this.player_url = innertube_data.PLAYER_JS_URL;
|
||||
this.logged_in = innertube_data.LOGGED_IN;
|
||||
this.sts = innertube_data.STS;
|
||||
this.context.client.hl = 'en';
|
||||
this.context.client.gl = 'US';
|
||||
|
||||
if (this.logged_in && this.cookie.length > 1) {
|
||||
this.auth_apisid = Utils.getStringBetweenStrings(this.cookie, 'PAPISID=', ';');
|
||||
this.auth_apisid = Utils.generateSidAuth(this.auth_apisid);
|
||||
this.player = new Player(this);
|
||||
await this.player.init();
|
||||
|
||||
if (this.logged_in && this.cookie.length > 1) {
|
||||
this.auth_apisid = Utils.getStringBetweenStrings(this.cookie, 'PAPISID=', ';');
|
||||
this.auth_apisid = Utils.generateSidAuth(this.auth_apisid);
|
||||
}
|
||||
} else {
|
||||
this.retry_count += 1;
|
||||
if (this.retry_count >= 10) throw new Error('Could not retrieve Innertube data');
|
||||
return this.init();
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
} else {
|
||||
this.initialized = false;
|
||||
} catch (err) {
|
||||
this.retry_count += 1;
|
||||
if (this.retry_count >= 10) throw new Error('Could not retrieve Innertube data');
|
||||
return this.init();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -93,43 +101,42 @@ class Innertube extends EventEmitter {
|
||||
|
||||
async search(query, options = { period: 'any', order: 'relevance', duration: 'any' }) {
|
||||
if (!query) throw new Error('No query was provided');
|
||||
if (!this.initialized) throw new Error('Missing Innertube data.');
|
||||
|
||||
const yt_response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/search${this.logged_in && this.cookie.length < 1 ? '' : `?key=${this.key}`}`, JSON.stringify({ context: this.context, params: Constants.filters(options.period + ',' + options.duration + ',' + options.order), query }), Constants.innertube_request_opts({ session: this })).catch((error) => error);
|
||||
if (yt_response instanceof Error) throw new Error('Could not search on YouTube: ' + yt_response.message);
|
||||
const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/search${this.logged_in && this.cookie.length < 1 ? '' : `?key=${this.key}`}`, JSON.stringify({ context: this.context, params: Constants.filters(options.period + ',' + options.duration + ',' + options.order), query }), Constants.INNERTUBE_REQOPTS({ session: this })).catch((error) => error);
|
||||
if (response instanceof Error) throw new Error(`Could not search on YouTube: ${response.message}`);
|
||||
|
||||
let content = yt_response.data.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents[0].itemSectionRenderer.contents;
|
||||
let content = response.data.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents[0].itemSectionRenderer.contents;
|
||||
let search_response = {};
|
||||
|
||||
search_response.search_metadata = {};
|
||||
search_response.search_metadata.query = content[0].showingResultsForRenderer ? content[0].showingResultsForRenderer.originalQuery.simpleText : query;
|
||||
search_response.search_metadata.corrected_query = content[0].showingResultsForRenderer ? content[0].showingResultsForRenderer.correctedQueryEndpoint.searchEndpoint.query : query;
|
||||
search_response.search_metadata.estimated_results = parseInt(yt_response.data.estimatedResults);
|
||||
search_response.search_metadata.estimated_results = parseInt(response.data.estimatedResults);
|
||||
search_response.videos = content.map((data) => {
|
||||
if (!data.videoRenderer) return;
|
||||
let video = data.videoRenderer;
|
||||
return {
|
||||
title: video.title.runs[0].text,
|
||||
description: video.detailedMetadataSnippets ? video.detailedMetadataSnippets[0].snippetText.runs.map((item) => item.text).join('') : 'N/A',
|
||||
description: video.detailedMetadataSnippets && video.detailedMetadataSnippets[0].snippetText.runs.map((item) => item.text).join('') || 'N/A',
|
||||
author: video.ownerText.runs[0].text,
|
||||
id: video.videoId,
|
||||
url: 'https://youtu.be/' + video.videoId,
|
||||
channel_url: Constants.urls.YT_BASE_URL + video.ownerText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url,
|
||||
url: `https://youtu.be/${video.videoId}`,
|
||||
channel_url: `${Constants.URLS.YT_BASE_URL}${video.ownerText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}`,
|
||||
metadata: {
|
||||
view_count: video.viewCountText ? video.viewCountText.simpleText : 'N/A',
|
||||
view_count: video.viewCountText && video.viewCountText.simpleText || 'N/A',
|
||||
short_view_count_text: {
|
||||
simple_text: video.shortViewCountText ? video.shortViewCountText.simpleText : 'N/A',
|
||||
accessibility_label: video.shortViewCountText ? (video.shortViewCountText.accessibility ? video.shortViewCountText.accessibility.accessibilityData.label : 'N/A') : 'N/A',
|
||||
simple_text: video.shortViewCountText && video.shortViewCountText.simpleText || 'N/A',
|
||||
accessibility_label: video.shortViewCountText && (video.shortViewCountText.accessibility && video.shortViewCountText.accessibility.accessibilityData.label || 'N/A') || 'N/A',
|
||||
},
|
||||
thumbnails: video.thumbnail.thumbnails,
|
||||
duration: {
|
||||
seconds: TimeToSeconds(video.lengthText ? video.lengthText.simpleText : '0'),
|
||||
simple_text: video.lengthText ? video.lengthText.simpleText : 'N/A',
|
||||
accessibility_label: video.lengthText ? video.lengthText.accessibility.accessibilityData.label : 'N/A'
|
||||
seconds: TimeToSeconds(video.lengthText && video.lengthText.simpleText || '0'),
|
||||
simple_text: video.lengthText && video.lengthText.simpleText || 'N/A',
|
||||
accessibility_label: video.lengthText && video.lengthText.accessibility.accessibilityData.label || 'N/A'
|
||||
},
|
||||
published: video.publishedTimeText ? video.publishedTimeText.simpleText : 'N/A',
|
||||
badges: video.badges ? video.badges.map((item) => item.metadataBadgeRenderer.label) : 'N/A',
|
||||
owner_badges: video.ownerBadges ? video.ownerBadges.map((item) => item.metadataBadgeRenderer.tooltip) : 'N/A '
|
||||
published: video.publishedTimeText && video.publishedTimeText.simpleText || 'N/A',
|
||||
badges: video.badges && video.badges.map((item) => item.metadataBadgeRenderer.label) || 'N/A',
|
||||
owner_badges: video.ownerBadges && video.ownerBadges.map((item) => item.metadataBadgeRenderer.tooltip) || 'N/A '
|
||||
}
|
||||
};
|
||||
}).filter((video_block) => video_block !== undefined);
|
||||
@@ -172,7 +179,7 @@ class Innertube extends EventEmitter {
|
||||
}
|
||||
|
||||
const response = await Actions.getContinuation(this, { continuation_token: comment_section_token || token });
|
||||
if (!response.success) throw new Error('Could not fetch comment section');
|
||||
if (!response.success) throw new Error('Could not fetch comments section');
|
||||
|
||||
const comments_section = { comments: [] };
|
||||
!token && (comments_section.comment_count = response.data.onResponseReceivedEndpoints[0].reloadContinuationItemsCommand.continuationItems && response.data.onResponseReceivedEndpoints[0].reloadContinuationItemsCommand.continuationItems[0].commentsHeaderRenderer.countText.runs[0].text || 'N/A');
|
||||
@@ -200,7 +207,7 @@ class Innertube extends EventEmitter {
|
||||
published: thread.commentThreadRenderer.comment.commentRenderer.publishedTimeText.runs[0].text,
|
||||
is_liked: thread.commentThreadRenderer.comment.commentRenderer.isLiked,
|
||||
is_channel_owner: thread.commentThreadRenderer.comment.commentRenderer.authorIsChannelOwner,
|
||||
like_count: thread.commentThreadRenderer.comment.commentRenderer.voteCount.simpleText,
|
||||
like_count: thread.commentThreadRenderer.comment.commentRenderer.voteCount && thread.commentThreadRenderer.comment.commentRenderer.voteCount.simpleText || 'N/A',
|
||||
reply_count: thread.commentThreadRenderer.comment.commentRenderer.replyCount || 0,
|
||||
id: thread.commentThreadRenderer.comment.commentRenderer.commentId,
|
||||
}
|
||||
@@ -278,11 +285,8 @@ class Innertube extends EventEmitter {
|
||||
|
||||
async requestVideoInfo(id, desktop) {
|
||||
let response;
|
||||
if (!desktop) {
|
||||
response = await Axios.get(`${Constants.urls.YT_WATCH_PAGE}?v=${id}&t=8s&pbj=1`, Constants.innertube_request_opts({ session: this, id, desktop: false })).catch((error) => error);
|
||||
} else {
|
||||
response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/player${this.logged_in && this.cookie.length < 1 ? '' : `?key=${this.key}`}`, JSON.stringify(Constants.video_details_reqbody(id, this.sts, this.context)), Constants.innertube_request_opts({ session: this, id, desktop: true })).catch((error) => error);
|
||||
}
|
||||
!desktop && (response = await Axios.get(`${Constants.URLS.YT_WATCH_PAGE}?v=${id}&t=8s&pbj=1`, Constants.INNERTUBE_REQOPTS({ session: this, id, desktop: false })).catch((error) => error)) ||
|
||||
(response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/player${this.logged_in && this.cookie.length < 1 ? '' : `?key=${this.key}`}`, JSON.stringify(Constants.VIDEO_INFO_REQBODY(id, this.sts, this.context)), Constants.INNERTUBE_REQOPTS({ session: this, id, desktop: true })).catch((error) => error));
|
||||
if (response instanceof Error) throw new Error('Could not retrieve watch page info: ' + response.message);
|
||||
return response.data;
|
||||
}
|
||||
@@ -352,15 +356,10 @@ class Innertube extends EventEmitter {
|
||||
if (options.type != 'videoandaudio') {
|
||||
let streams;
|
||||
|
||||
if (options.type != 'audio') {
|
||||
streams = filtered_streams.filter((format) => format.mimeType.includes(options.format || 'mp4') && format.qualityLabel == options.quality);
|
||||
} else {
|
||||
streams = filtered_streams.filter((format) => format.mimeType.includes(options.format || 'mp4'));
|
||||
}
|
||||
options.type != 'audio' && (streams = filtered_streams.filter((format) => format.mimeType.includes(options.format || 'mp4') && format.qualityLabel == options.quality)) ||
|
||||
(streams = filtered_streams.filter((format) => format.mimeType.includes(options.format || 'mp4')));
|
||||
|
||||
if (streams == undefined || streams.length == 0) {
|
||||
streams = filtered_streams.filter((format) => format.quality == 'medium');
|
||||
}
|
||||
streams == undefined || streams.length == 0 && (streams = filtered_streams.filter((format) => format.quality == 'medium'));
|
||||
|
||||
bitrates = streams.map((format) => format.bitrate);
|
||||
url = streams.filter((format) => format.bitrate === Math.max(...bitrates))[0];
|
||||
@@ -368,16 +367,16 @@ class Innertube extends EventEmitter {
|
||||
|
||||
const selected_format = options.type == 'videoandaudio' ? filtered_streams[0] : url;
|
||||
if (!selected_format) {
|
||||
return stream.emit('error', { message: 'Could not find any suitable format.', type: 'FORMATS_UNAVAILABLE' });
|
||||
return stream.emit('error', { message: 'Could not find any suitable format.', type: 'FORMAT_UNAVAILABLE' });
|
||||
} else {
|
||||
stream.emit('info', { video_details, selected_format, formats });
|
||||
}
|
||||
|
||||
if (options.type == 'videoandaudio') {
|
||||
if (options.type == 'videoandaudio' && !options.range) {
|
||||
const response = await Axios.get(selected_format.url, {
|
||||
responseType: 'stream',
|
||||
cancelToken: new CancelToken(function executor(c) { cancel = c; }),
|
||||
headers: Constants.stream_headers()
|
||||
headers: Constants.STREAM_HEADERS
|
||||
}).catch((error) => error);
|
||||
|
||||
if (response instanceof Error) {
|
||||
@@ -406,21 +405,22 @@ class Innertube extends EventEmitter {
|
||||
response.data.pipe(stream, { end: true });
|
||||
} else {
|
||||
const chunk_size = 1048576 * 10; // 10MB
|
||||
|
||||
let chunk_start = 0;
|
||||
let chunk_end = chunk_size;
|
||||
|
||||
let chunk_start = (options.range && options.range.start || 0);
|
||||
let chunk_end = (options.range && options.range.end || chunk_size);
|
||||
let downloaded_size = 0;
|
||||
let end = false;
|
||||
let must_end = false;
|
||||
|
||||
stream.emit('start');
|
||||
|
||||
const downloadChunk = async () => {
|
||||
if (chunk_end >= selected_format.contentLength) end = true;
|
||||
(chunk_end >= selected_format.contentLength || options.range) && (must_end = true);
|
||||
options.range && (selected_format.contentLength = options.range.end);
|
||||
|
||||
const response = await Axios.get(`${selected_format.url}&range=${chunk_start}-${chunk_end || ''}`, {
|
||||
responseType: 'stream',
|
||||
cancelToken: new CancelToken(function executor(c) { cancel = c; }),
|
||||
headers: Constants.stream_headers()
|
||||
headers: Constants.STREAM_HEADERS
|
||||
}).catch((error) => error);
|
||||
|
||||
if (response instanceof Error) {
|
||||
@@ -444,14 +444,14 @@ class Innertube extends EventEmitter {
|
||||
});
|
||||
|
||||
response.data.on('end', () => {
|
||||
if (!end) {
|
||||
if (!must_end && !options.range) {
|
||||
chunk_start = chunk_end + 1;
|
||||
chunk_end += chunk_size;
|
||||
downloadChunk();
|
||||
}
|
||||
});
|
||||
|
||||
response.data.pipe(stream, { end });
|
||||
response.data.pipe(stream, { end: must_end });
|
||||
};
|
||||
downloadChunk();
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class Livechat extends EventEmitter {
|
||||
|
||||
enqueueActionGroup(group) {
|
||||
group.forEach((action) => {
|
||||
if (!action.addChatItemAction) return; //TODO: handle different action types
|
||||
if (!action.addChatItemAction) return; //TODO: handle different action types
|
||||
const message_content = action.addChatItemAction.item.liveChatTextMessageRenderer;
|
||||
if (!message_content) return;
|
||||
|
||||
@@ -49,14 +49,14 @@ class Livechat extends EventEmitter {
|
||||
let data;
|
||||
|
||||
data = { context: this.session.context, continuation: this.ctoken };
|
||||
const livechat = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/live_chat/get_live_chat${this.session.logged_in && this.session.cookie.length < 1 ? '' : `?key=${this.session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session: this.session, data, desktop: true }));
|
||||
const livechat = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/live_chat/get_live_chat${this.session.logged_in && this.session.cookie.length < 1 ? '' : `?key=${this.session.key}`}`, JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session: this.session, data, desktop: true }));
|
||||
if (livechat instanceof Error) throw new Error(`Error polling livechat: ${livechat.message}`);
|
||||
|
||||
const continuation_contents = livechat.data.continuationContents;
|
||||
const action_group = continuation_contents.liveChatContinuation.actions;
|
||||
this.enqueueActionGroup(action_group);
|
||||
|
||||
// Why don't we just emit the message directly? Well, enqueueing the messages is necessary so they are not emitted in a “messy” way, funny enough that's exactly how YouTube does it in its livechat js player.
|
||||
// Why don't we just emit the message directly? Well, enqueueing the messages is necessary so they are not emitted in a “messy” way, funny enough that's exactly how YouTube does it in its live chat js script.
|
||||
this.message_queue.forEach((message, index) => {
|
||||
if (this.id_cache.includes(message.id)) return;
|
||||
setTimeout(() => this.emit('chat-update', message), message.timestamp / 1000 - new Date().getTime());
|
||||
@@ -68,7 +68,7 @@ class Livechat extends EventEmitter {
|
||||
data = { context: this.session.context, videoId: this.video_id };
|
||||
if (this.metadata_ctoken) data.continuation = this.metadata_ctoken;
|
||||
|
||||
const updated_metadata = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/updated_metadata${this.session.logged_in && this.session.cookie.length < 1 ? '' : `?key=${this.session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session: this.session, data, desktop: true }));
|
||||
const updated_metadata = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/updated_metadata${this.session.logged_in && this.session.cookie.length < 1 ? '' : `?key=${this.session.key}`}`, JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session: this.session, data, desktop: true }));
|
||||
if (updated_metadata instanceof Error) throw new Error(`Error polling updated metadata: ${updated_metadata.message}`);
|
||||
this.metadata_ctoken = updated_metadata.data.continuation.timedContinuationData.continuation;
|
||||
|
||||
|
||||
148
lib/NToken.js
148
lib/NToken.js
@@ -7,123 +7,105 @@ class NToken {
|
||||
constructor(raw_code) {
|
||||
this.raw_code = raw_code;
|
||||
this.null_placeholder_regex = /c\[(.*?)\]=c/g;
|
||||
this.transformation_args_regex = /c\[(.*?)\]\((.+?)\)/g;
|
||||
this.transformation_calls_regex = /c\[(.*?)\]\((.+?)\)/g;
|
||||
}
|
||||
|
||||
transform(n) {
|
||||
let n_token = n.split('');
|
||||
let transformations = this.getTransformationData(this.raw_code);
|
||||
|
||||
// Identifies the necessary transformation functions and emulates them accordingly.
|
||||
transformations = transformations.map((el) => {
|
||||
if (el != null && typeof el != 'number') {
|
||||
const is_reverse_base64 = el.includes('case 65:');
|
||||
if (el.includes('function(d){for(var')) {
|
||||
el = (arr) => this.pushSplice(arr);
|
||||
} else if (el.includes('d.push(e)')) {
|
||||
el = (arr, item) => this.push(arr, item);
|
||||
} else if (el.includes('d.reverse()')) {
|
||||
el = (arr) => this.reverse(arr);
|
||||
} else if (el.includes('d.length;d.splice(e,1)')) {
|
||||
el = (arr, index) => this.spliceOnce(arr, index);
|
||||
} else if (el.includes('d[0])[0])')) {
|
||||
el = (arr, index) => this.spliceTwice(arr, index);
|
||||
} else if (el.includes('reverse().forEach')) {
|
||||
el = (arr, index) => this.spliceReverseUnshift(arr, index);
|
||||
} else if (el.includes('f=d[0];d[0]')) {
|
||||
el = (arr, index) => this.swapFirstItem(arr, index);
|
||||
} else if (el.includes('unshift(d.pop())')) {
|
||||
el = (arr, index) => this.unshiftPop(arr, index);
|
||||
} else if (el.includes('switch')) {
|
||||
el = (arr, e) => this.translateAB(arr, e, is_reverse_base64);
|
||||
} else if (el === 'b') {
|
||||
el = n_token;
|
||||
try {
|
||||
let transformations = this.getTransformationData(this.raw_code);
|
||||
transformations = transformations.map((el) => {
|
||||
if (el != null && typeof el != 'number') {
|
||||
const is_reverse_base64 = el.includes('case 65:');
|
||||
(({ // Identifies the transformation functions and emulates them accordingly.
|
||||
[Constants.FUNCS.PUSH]: () => el = (arr, i) => this.push(arr, i),
|
||||
[Constants.FUNCS.SPLICE]: () => el = (arr, i) => this.splice(arr, i),
|
||||
[Constants.FUNCS.SWAP0_1]: () => el = (arr, i) => this.swap0(arr, i),
|
||||
[Constants.FUNCS.SWAP0_2]: () => el = (arr, i) => this.swap0(arr, i),
|
||||
[Constants.FUNCS.ROTATE_1]: () => el = (arr, i) => this.rotate(arr, i),
|
||||
[Constants.FUNCS.ROTATE_2]: () => el = (arr, i) => this.rotate(arr, i),
|
||||
[Constants.FUNCS.REVERSE_1]: () => el = (arr) => this.reverse(arr),
|
||||
[Constants.FUNCS.REVERSE_2]: () => el = (arr) => this.reverse(arr),
|
||||
[Constants.FUNCS.BASE64_DIA]: () => el = () => this.getBase64Dia(is_reverse_base64),
|
||||
[Constants.FUNCS.TRANSLATE_1]: () => el = (arr, token) => this.translate1(arr, token, is_reverse_base64),
|
||||
[Constants.FUNCS.TRANSLATE_2]: () => el = (arr, token, base64_dic) => this.translate2(arr, token, base64_dic)
|
||||
})[this.getFunc(el)] || (() => el === 'b' && (el = n_token)))();
|
||||
}
|
||||
}
|
||||
return el;
|
||||
});
|
||||
return el;
|
||||
});
|
||||
|
||||
// Fills the null placeholders with a copy of the transformations array.
|
||||
let null_placeholder_positions = [...this.raw_code.matchAll(this.null_placeholder_regex)].map((item) => parseInt(item[1]));
|
||||
null_placeholder_positions.forEach((pos) => transformations[pos] = transformations);
|
||||
// Fills the null placeholders with a copy of the transformations array.
|
||||
let null_placeholder_positions = [...this.raw_code.matchAll(this.null_placeholder_regex)].map((item) => parseInt(item[1]));
|
||||
null_placeholder_positions.forEach((pos) => transformations[pos] = transformations);
|
||||
|
||||
// Parses and emulates calls to functions of the transformations array.
|
||||
let transformation_args = [...Utils.getStringBetweenStrings(this.raw_code.replace(/\n/g, ''), 'try{', '}catch').matchAll(this.transformation_args_regex)].map((params) => ({ index: params[1], params: params[2] }));
|
||||
transformation_args.forEach((data) => {
|
||||
const index = data.index;
|
||||
const param_index = data.params.split(',').map((param) => param.match(/c\[(.*?)\]/)[1]);
|
||||
transformations[index](transformations[param_index[0]], transformations[param_index[1]]);
|
||||
});
|
||||
// Parses and emulates calls to functions of the transformations array.
|
||||
let transformation_calls = [...Utils.getStringBetweenStrings(this.raw_code.replace(/\n/g, ''), 'try{', '}catch').matchAll(this.transformation_calls_regex)].map((params) => ({ index: params[1], params: params[2] }));
|
||||
transformation_calls.forEach((data) => {
|
||||
const param_index = data.params.split(',').map((param) => param.match(/c\[(.*?)\]/)[1]);
|
||||
const base64_dia = (param_index[2] && transformations[param_index[2]]());
|
||||
transformations[data.index](transformations[param_index[0]], transformations[param_index[1]], base64_dia);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Could not transform n-token, download may be throttled:', err);
|
||||
return n;
|
||||
}
|
||||
|
||||
return n_token.join('');
|
||||
}
|
||||
|
||||
getTransformationData() {
|
||||
let transformation_data = '[' + Utils.getStringBetweenStrings(this.raw_code, 'c=[', '];c') + ']';
|
||||
transformation_data = transformation_data
|
||||
.replace(/function\(d,e\)/g, '"function(d,e)')
|
||||
.replace(/function\(d\)/g, '"function(d)')
|
||||
.replace(/,b,/g, ',"b",')
|
||||
.replace(/,b/g, ',"b"')
|
||||
.replace(/b,/g, '"b",')
|
||||
.replace(/b]/g, '"b"]')
|
||||
.replace(/},/g, '}",')
|
||||
.replace(/""/g, '')
|
||||
.replace(/length]\)}"/g, 'length])}');
|
||||
|
||||
return JSON.parse(transformation_data);
|
||||
getFunc(el) {
|
||||
return el.match(Constants.FUNCS_REGEX);
|
||||
}
|
||||
|
||||
translateAB(arr, e, is_reverse_base64) {
|
||||
let characters = is_reverse_base64 && Constants.base64_alphabet.reverse || Constants.base64_alphabet.normal;
|
||||
getTransformationData() {
|
||||
const data = `[${Utils.getStringBetweenStrings(this.raw_code.replace(/\n/g, ''), 'c=[', '];c')}]`;
|
||||
return JSON.parse(Constants.formatNTransformData(data));
|
||||
}
|
||||
|
||||
translate1(arr, token, is_reverse_base64) {
|
||||
const characters = is_reverse_base64 && Constants.BASE64_DIALECT.REVERSE || Constants.BASE64_DIALECT.NORMAL;
|
||||
arr.forEach(function(char, index, loc) {
|
||||
this.push(loc[index] = characters[(characters.indexOf(char) - characters.indexOf(this[index]) + 64) % characters.length]);
|
||||
}, e.split(''));
|
||||
}, token.split(''));
|
||||
}
|
||||
|
||||
unshiftPop(arr, index) {
|
||||
index = (index % arr.length + arr.length) % arr.length;
|
||||
for (; index--;) {
|
||||
arr.unshift(arr.pop());
|
||||
}
|
||||
translate2(arr, token, characters) {
|
||||
let chars_length = characters.length;
|
||||
arr.forEach(function(char, index, loc) {
|
||||
this.push(loc[index] = characters[(characters.indexOf(char) - characters.indexOf(this[index]) + index + chars_length--) % characters.length]);
|
||||
}, token.split(''));
|
||||
}
|
||||
|
||||
swapFirstItem(arr, index) {
|
||||
let oldValue = arr[0];
|
||||
getBase64Dia(is_reverse_base64) {
|
||||
const characters = is_reverse_base64 && Constants.BASE64_DIALECT.REVERSE || Constants.BASE64_DIALECT.NORMAL;
|
||||
return characters;
|
||||
}
|
||||
|
||||
swap0(arr, index) {
|
||||
const old_value = arr[0];
|
||||
index = (index % arr.length + arr.length) % arr.length;
|
||||
arr[0] = arr[index];
|
||||
arr[index] = oldValue;
|
||||
arr[index] = old_value;
|
||||
}
|
||||
|
||||
spliceReverseUnshift(arr, index) {
|
||||
rotate(arr, index) {
|
||||
index = (index % arr.length + arr.length) % arr.length;
|
||||
arr.splice(-index).reverse().forEach(function(f) {
|
||||
arr.unshift(f);
|
||||
});
|
||||
arr.splice(-index).reverse().forEach((el) => arr.unshift(el));
|
||||
}
|
||||
|
||||
spliceOnce(arr, index) {
|
||||
splice(arr, index) {
|
||||
index = (index % arr.length + arr.length) % arr.length;
|
||||
arr.splice(index, 1);
|
||||
}
|
||||
|
||||
spliceTwice(arr, index) {
|
||||
index = (index % arr.length + arr.length) % arr.length;
|
||||
arr.splice(0, 1, arr.splice(index, 1, arr[0])[0]);
|
||||
}
|
||||
|
||||
pushSplice(arr) {
|
||||
for (let index = arr.length; index;)
|
||||
arr.push(arr.splice(--index, 1)[0]);
|
||||
}
|
||||
|
||||
push(arr, item) {
|
||||
arr.push(item);
|
||||
}
|
||||
|
||||
reverse(arr) {
|
||||
arr.reverse();
|
||||
}
|
||||
|
||||
push(arr, item) {
|
||||
arr.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NToken;
|
||||
118
lib/OAuth.js
118
lib/OAuth.js
@@ -13,27 +13,75 @@ class OAuth extends EventEmitter {
|
||||
this.refresh_interval = 5;
|
||||
|
||||
// OAuth URLs:
|
||||
this.oauth_code_url = `${Constants.urls.YT_BASE_URL}/o/oauth2/device/code`;
|
||||
this.oauth_token_url = `${Constants.urls.YT_BASE_URL}/o/oauth2/token`;
|
||||
this.oauth_code_url = `${Constants.URLS.YT_BASE_URL}/o/oauth2/device/code`;
|
||||
this.oauth_token_url = `${Constants.URLS.YT_BASE_URL}/o/oauth2/token`;
|
||||
|
||||
// Used to check whether an access token is valid or not.
|
||||
this.guide_url = `${Constants.urls.YT_BASE_URL}/youtubei/v1/guide`;
|
||||
this.guide_url = `${Constants.URLS.YT_BASE_URL}/youtubei/v1/guide`;
|
||||
|
||||
// These are always the same, so we shouldn't have any problems for now.
|
||||
this.model_name = Constants.oauth.model_name;
|
||||
this.grant_type = Constants.oauth.grant_type;
|
||||
this.scope = Constants.oauth.scope;
|
||||
this.model_name = Constants.OAUTH.MODEL_NAME;
|
||||
this.grant_type = Constants.OAUTH.GRANT_TYPE;
|
||||
this.scope = Constants.OAUTH.SCOPE;
|
||||
|
||||
// Script that contains important information such as client id and client secret.
|
||||
this.auth_script_regex = /<script id=\"base-js\" src=\"(.*?)\" nonce=".*?"><\/script>/;
|
||||
|
||||
// Used to find the credentials inside the script.
|
||||
this.identity_regex = /var .+?=\"(?<id>.+?)\",[.|\s].?=\"(?<secret>.+?)\"/;
|
||||
this.identity_regex = /var .+?=\"(?<id>.+?)\",.+?=\"(?<secret>.+?)\"/;
|
||||
|
||||
if (creds.access_token != undefined && creds.refresh_token != undefined) return;
|
||||
this.requestAuthCode();
|
||||
}
|
||||
|
||||
async requestAuthCode() {
|
||||
const identity = await this.getClientIdentity();
|
||||
this.client_id = identity.id;
|
||||
this.client_secret = identity.secret;
|
||||
|
||||
const data = {
|
||||
client_id: this.client_id,
|
||||
scope: this.scope,
|
||||
device_id: Uuid.v4(),
|
||||
model_name: this.model_name
|
||||
};
|
||||
|
||||
const response = await Axios.post(this.oauth_code_url, JSON.stringify(data), Constants.OAUTH.HEADERS).catch((error) => error);
|
||||
if (response instanceof Error)
|
||||
return this.emit('auth', {
|
||||
error: 'Could not get auth code.',
|
||||
status: 'FAILED'
|
||||
});
|
||||
|
||||
this.emit('auth', {
|
||||
code: response.data.user_code,
|
||||
status: 'AUTHORIZATION_PENDING',
|
||||
expires_in: response.data.expires_in,
|
||||
verification_url: response.data.verification_url
|
||||
});
|
||||
|
||||
this.refresh_interval = response.data.interval;
|
||||
|
||||
// Keeps requesting at a specific rate until the authorization is granted or denied.
|
||||
this.waitForAuth(response.data.device_code);
|
||||
}
|
||||
|
||||
async getClientIdentity() {
|
||||
// The first request is made to get the auth script url, hard-coding it isn't viable as it changes overtime.
|
||||
const yttv_response = await Axios.get(`${Constants.URLS.YT_BASE_URL}/tv`, Constants.OAUTH.HEADERS).catch((error) => error);
|
||||
if (yttv_response instanceof Error) throw new Error(`Could not extract client identify: ${yttv_response.message}`);
|
||||
|
||||
// Here we get the script and extract the necessary data to proceed with the auth flow.
|
||||
const url_body = this.auth_script_regex.exec(yttv_response.data)[1];
|
||||
const script_url = `${Constants.URLS.YT_BASE_URL}/${url_body}`;
|
||||
const response = await Axios.get(script_url, Constants.DEFAULT_HEADERS).catch((error) => error);
|
||||
if (response instanceof Error) throw new Error(`Could not extract client identify: ${response.message}`);
|
||||
|
||||
const identity_function = Utils.getStringBetweenStrings(response.data, 'setQuery("");', '{useGaiaSandbox:');
|
||||
const client_identity = identity_function.replace(/\n/g, '').match(this.identity_regex);
|
||||
return client_identity.groups;
|
||||
}
|
||||
|
||||
waitForAuth(device_code) {
|
||||
const data = {
|
||||
client_id: this.client_id,
|
||||
@@ -43,7 +91,7 @@ class OAuth extends EventEmitter {
|
||||
};
|
||||
|
||||
setTimeout(async () => {
|
||||
const response = await Axios.post(this.oauth_token_url, JSON.stringify(data), Constants.oauth_reqopts).catch((error) => error);
|
||||
const response = await Axios.post(this.oauth_token_url, JSON.stringify(data), Constants.OAUTH.HEADERS).catch((error) => error);
|
||||
if (response instanceof Error)
|
||||
return this.emit('auth', {
|
||||
error: 'Could not get auth token.',
|
||||
@@ -84,54 +132,6 @@ class OAuth extends EventEmitter {
|
||||
}, 1000 * this.refresh_interval);
|
||||
}
|
||||
|
||||
async requestAuthCode() {
|
||||
const identity = await this.getClientIdentity();
|
||||
this.client_id = identity.id;
|
||||
this.client_secret = identity.secret;
|
||||
|
||||
const data = {
|
||||
client_id: this.client_id,
|
||||
scope: this.scope,
|
||||
device_id: Uuid.v4(),
|
||||
model_name: this.model_name
|
||||
};
|
||||
|
||||
const response = await Axios.post(this.oauth_code_url, JSON.stringify(data), Constants.oauth_reqopts).catch((error) => error);
|
||||
if (response instanceof Error)
|
||||
return this.emit('auth', {
|
||||
error: 'Could not get auth code.',
|
||||
status: 'FAILED'
|
||||
});
|
||||
|
||||
this.emit('auth', {
|
||||
code: response.data.user_code,
|
||||
status: 'AUTHORIZATION_PENDING',
|
||||
expires_in: response.data.expires_in,
|
||||
verification_url: response.data.verification_url
|
||||
});
|
||||
|
||||
this.refresh_interval = response.data.interval;
|
||||
|
||||
// Keeps requesting at a specific rate until the authorization is granted or denied.
|
||||
this.waitForAuth(response.data.device_code);
|
||||
}
|
||||
|
||||
async getClientIdentity() {
|
||||
// The first request is made to get the auth script url, hard-coding it isn't viable as it changes overtime.
|
||||
const yttv_response = await Axios.get(`${Constants.urls.YT_BASE_URL}/tv`, Constants.oauth_reqopts).catch((error) => error);
|
||||
if (yttv_response instanceof Error) throw new Error(`Could not get identify: ${yttv_response.message}`);
|
||||
|
||||
// Here we get the script and extract the necessary data to proceed with the auth flow.
|
||||
const url_body = this.auth_script_regex.exec(yttv_response.data)[1];
|
||||
const script_url = `${Constants.urls.YT_BASE_URL}/${url_body}`;
|
||||
const response = await Axios.get(script_url, Constants.default_headers).catch((error) => error);
|
||||
if (response instanceof Error) throw new Error(`Could not fetch data from auth script: ${response.message}`);
|
||||
|
||||
const identity_function = Utils.getStringBetweenStrings(response.data, '=function(){var a=window.environment', '(function()');
|
||||
const client_identity = identity_function.match(this.identity_regex).groups;
|
||||
return client_identity;
|
||||
}
|
||||
|
||||
async refreshAccessToken(refresh_token) {
|
||||
const identity = await this.getClientIdentity();
|
||||
|
||||
@@ -142,10 +142,10 @@ class OAuth extends EventEmitter {
|
||||
grant_type: 'refresh_token',
|
||||
};
|
||||
|
||||
const response = await Axios.post(this.oauth_token_url, JSON.stringify(data), Constants.oauth_reqopts).catch((error) => error);
|
||||
const response = await Axios.post(this.oauth_token_url, JSON.stringify(data), Constants.OAUTH.HEADERS).catch((error) => error);
|
||||
if (response instanceof Error)
|
||||
return this.emit('refresh-token', {
|
||||
error: 'Could not refresh token.',
|
||||
error: 'Could not refresh access token.',
|
||||
status: 'FAILED'
|
||||
});
|
||||
|
||||
@@ -159,7 +159,7 @@ class OAuth extends EventEmitter {
|
||||
}
|
||||
|
||||
async checkTokenValidity(access_token, session) {
|
||||
let headers = Constants.innertube_request_opts({ session }).headers;
|
||||
let headers = Constants.INNERTUBE_REQOPTS({ session }).headers;
|
||||
headers.authorization = `Bearer ${access_token}`;
|
||||
|
||||
const response = await Axios.post(this.guide_url, JSON.stringify({ context: session.context }), { headers }).catch((error) => error);
|
||||
|
||||
@@ -18,11 +18,14 @@ class Player {
|
||||
this.getSigDecipherCode(player_data);
|
||||
this.getNEncoder(player_data);
|
||||
} else {
|
||||
const response = await Axios.get(`${Constants.urls.YT_BASE_URL}${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 get player data: ' + response.message);
|
||||
const response = await Axios.get(`${Constants.URLS.YT_BASE_URL}${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);
|
||||
|
||||
fs.mkdirSync(this.tmp_cache_dir, { recursive: true });
|
||||
fs.writeFileSync(`${this.tmp_cache_dir}/${this.player_name}.js`, response.data);
|
||||
try {
|
||||
// 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.getSigDecipherCode(response.data);
|
||||
this.getNEncoder(response.data);
|
||||
@@ -36,7 +39,7 @@ class Player {
|
||||
}
|
||||
|
||||
getNEncoder(data) {
|
||||
this.ntoken_sc = 'var b=a.split("")' + Utils.getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}') + '} return b.join("");';
|
||||
this.ntoken_sc = `var b=a.split("")${Utils.getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}')}} return b.join("");`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,10 +20,10 @@ class SigDecipher {
|
||||
arr.splice(0, end);
|
||||
}
|
||||
|
||||
function swap(arr, position) {
|
||||
function swap(arr, index) {
|
||||
let origArrI = arr[0];
|
||||
arr[0] = arr[position % arr.length];
|
||||
arr[position % arr.length] = origArrI;
|
||||
arr[0] = arr[index % arr.length];
|
||||
arr[index % arr.length] = origArrI;
|
||||
}
|
||||
|
||||
function reverse(arr) {
|
||||
@@ -49,7 +49,6 @@ class SigDecipher {
|
||||
}
|
||||
|
||||
const url_components = new URL(args.url);
|
||||
|
||||
args.sp !== undefined ? url_components.searchParams.set(args.sp, signature.join('')) : url_components.searchParams.set('signature', signature.join(''));
|
||||
url_components.searchParams.set('cver', this.cver);
|
||||
url_components.searchParams.set('ratebypass', 'yes');
|
||||
42
package-lock.json
generated
42
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.6",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^0.21.4",
|
||||
@@ -54,9 +54,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.14.4",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
|
||||
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==",
|
||||
"version": "1.14.5",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
|
||||
"integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -78,9 +78,9 @@
|
||||
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
|
||||
},
|
||||
"node_modules/multiformats": {
|
||||
"version": "9.4.9",
|
||||
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.4.9.tgz",
|
||||
"integrity": "sha512-zA84TTJcRfRMpjvYqy63piBbSEdqlIGqNNSpP6kspqtougqjo60PRhIFo+oAxrjkof14WMCImvr7acK6rPpXLw=="
|
||||
"version": "9.4.10",
|
||||
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.4.10.tgz",
|
||||
"integrity": "sha512-BwWGvgqB/5J/cnWaOA0sXzJ+UGl+kyFAw3Sw1L6TN4oad34C9OpW+GCpYTYPDp4pUaXDC1EjvB3yv9Iodo1EhA=="
|
||||
},
|
||||
"node_modules/protocol-buffers-schema": {
|
||||
"version": "3.6.0",
|
||||
@@ -137,9 +137,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/user-agents": {
|
||||
"version": "1.0.814",
|
||||
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.814.tgz",
|
||||
"integrity": "sha512-OU3lCkkaUCghnZiZuN2edSJ+//ituJihs3P1FCjCDwe1tMBS8eni68CQ0TBbPoVPDtoAqxM3Z7qbP7EWGlhBxA==",
|
||||
"version": "1.0.840",
|
||||
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.840.tgz",
|
||||
"integrity": "sha512-pdXizW5mvcHRHsJH50STUA+zywDvUKbxouQ2523FEmfYJCkLMVGavBSOL7u/VVmneVXM2EX20Ikru9lCnbNDeQ==",
|
||||
"dependencies": {
|
||||
"dot-json": "^1.2.2",
|
||||
"lodash.clonedeep": "^4.5.0"
|
||||
@@ -189,9 +189,9 @@
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.14.4",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
|
||||
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g=="
|
||||
"version": "1.14.5",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
|
||||
"integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA=="
|
||||
},
|
||||
"lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
@@ -199,9 +199,9 @@
|
||||
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
|
||||
},
|
||||
"multiformats": {
|
||||
"version": "9.4.9",
|
||||
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.4.9.tgz",
|
||||
"integrity": "sha512-zA84TTJcRfRMpjvYqy63piBbSEdqlIGqNNSpP6kspqtougqjo60PRhIFo+oAxrjkof14WMCImvr7acK6rPpXLw=="
|
||||
"version": "9.4.10",
|
||||
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.4.10.tgz",
|
||||
"integrity": "sha512-BwWGvgqB/5J/cnWaOA0sXzJ+UGl+kyFAw3Sw1L6TN4oad34C9OpW+GCpYTYPDp4pUaXDC1EjvB3yv9Iodo1EhA=="
|
||||
},
|
||||
"protocol-buffers-schema": {
|
||||
"version": "3.6.0",
|
||||
@@ -254,9 +254,9 @@
|
||||
}
|
||||
},
|
||||
"user-agents": {
|
||||
"version": "1.0.814",
|
||||
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.814.tgz",
|
||||
"integrity": "sha512-OU3lCkkaUCghnZiZuN2edSJ+//ituJihs3P1FCjCDwe1tMBS8eni68CQ0TBbPoVPDtoAqxM3Z7qbP7EWGlhBxA==",
|
||||
"version": "1.0.840",
|
||||
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.840.tgz",
|
||||
"integrity": "sha512-pdXizW5mvcHRHsJH50STUA+zywDvUKbxouQ2523FEmfYJCkLMVGavBSOL7u/VVmneVXM2EX20Ikru9lCnbNDeQ==",
|
||||
"requires": {
|
||||
"dot-json": "^1.2.2",
|
||||
"lodash.clonedeep": "^4.5.0"
|
||||
@@ -273,4 +273,4 @@
|
||||
"integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.6",
|
||||
"description": "An object-oriented library that allows you to search, get detailed info about videos, subscribe, unsubscribe, like, dislike, comment, download videos and much more!",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "node test"
|
||||
},
|
||||
"author": "LuanRT",
|
||||
"license": "MIT",
|
||||
@@ -43,4 +43,4 @@
|
||||
"url": "https://github.com/LuanRT/YouTube.js/issues"
|
||||
},
|
||||
"homepage": "https://github.com/LuanRT/YouTube.js#readme"
|
||||
}
|
||||
}
|
||||
25
test/constants.js
Normal file
25
test/constants.js
Normal file
@@ -0,0 +1,25 @@
|
||||
'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=omhIaB28Jepv6Q&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&cver=2.20211101.01.00',
|
||||
original_ntoken: 'PqjqqJjdB9K821VIisj',
|
||||
expected_ntoken: 'AxwyS-osUl1WhMUd1',
|
||||
client_version: '2.20211101.01.00',
|
||||
test_video_id: 'FT_nzxtgXEw',
|
||||
test_video_id_1: 'YE7VzlLtp-4',
|
||||
sig_decipher_sc: `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)})},
|
||||
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())},
|
||||
2126741474,function(d){for(var e=d.length;e;)d.push(d.splice(--e,1)[0])},
|
||||
-1874551858,-1238260579,498106911,1913911047,-1951114300,-504396507,b,344510945,905306344,b,function(d,e){e=(e%d.length+d.length)%d.length;var f=d[0];d[0]=d[e];d[e]=f},
|
||||
909033134,1027812119,1686673079,function(d,e){d.push(e)},
|
||||
-1902376100,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(e,1)},
|
||||
"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("");`
|
||||
};
|
||||
57
test/index.js
Normal file
57
test/index.js
Normal file
@@ -0,0 +1,57 @@
|
||||
'use strict';
|
||||
|
||||
const Fs = require('fs');
|
||||
const Innertube = require('..');
|
||||
const NToken = require('../lib/NToken');
|
||||
const SigDecipher = require('../lib/Sig');
|
||||
const Constants = require('./constants');
|
||||
|
||||
let failed_tests = 0;
|
||||
|
||||
async function performTests() {
|
||||
const youtube = await new Innertube().catch((error) => error);
|
||||
assert(youtube instanceof Error ? false : true, `should retrieve Innertube configuration data`);
|
||||
|
||||
if (!(youtube instanceof Error)) {
|
||||
const search = await youtube.search('Carl Sagan - Documentary').catch((error) => error);
|
||||
assert((search instanceof Error ? false : true) && search.videos.length >= 1, `should search videos`);
|
||||
|
||||
const details = await youtube.getDetails(Constants.test_video_id).catch((error) => error);
|
||||
assert(details instanceof Error ? false : true, `should retrieve details for ${Constants.test_video_id}`);
|
||||
|
||||
const comments = await youtube.getComments(Constants.test_video_id).catch((error) => error);
|
||||
assert(comments instanceof Error ? false : true, `should retrieve comments for ${Constants.test_video_id}`);
|
||||
|
||||
const video = await downloadVideo(Constants.test_video_id_1, youtube).catch((error) => error);
|
||||
assert(video instanceof Error ? false : true, `should download video (${Constants.test_video_id_1})`);
|
||||
}
|
||||
|
||||
const n_token = new NToken(Constants.n_scramble_sc).transform(Constants.original_ntoken);
|
||||
assert(n_token == Constants.expected_ntoken, `should transform n token into ${Constants.expected_ntoken}`);
|
||||
|
||||
const transformed_url = new SigDecipher(Constants.test_url, Constants.client_version, { sig_decipher_sc: Constants.sig_decipher_sc, ntoken_sc: Constants.n_scramble_sc }).decipher();
|
||||
assert(transformed_url == Constants.expected_url, `should correctly decipher signature`);
|
||||
|
||||
if (failed_tests > 0)
|
||||
throw new Error('Some 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', () => reject());
|
||||
});
|
||||
}
|
||||
|
||||
function assert(outcome, description) {
|
||||
const pass_fail = outcome ? 'pass' : 'fail';
|
||||
!outcome && (failed_tests += 1);
|
||||
console.info(pass_fail, ':', description);
|
||||
return outcome;
|
||||
};
|
||||
|
||||
performTests();
|
||||
Reference in New Issue
Block a user