mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 17:42:18 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b095044baa | ||
|
|
ba2b757fdb | ||
|
|
9d7d0d83e1 | ||
|
|
b893e46634 | ||
|
|
d8ab6f3887 | ||
|
|
eea5ebfd04 | ||
|
|
b2117f11b9 | ||
|
|
389b0f362f | ||
|
|
6ce4a89766 | ||
|
|
4d7573c46f | ||
|
|
445de3546d | ||
|
|
3b265119d6 |
22
README.md
22
README.md
@@ -128,7 +128,7 @@ const search = await youtube.search('Looking for life on Mars - Documentary');
|
||||
|
||||
|
||||
|
||||
Getting details about a specific video:
|
||||
Get details about a specific video:
|
||||
|
||||
```js
|
||||
const video = await youtube.getDetails(search.videos[0].id);
|
||||
@@ -188,7 +188,7 @@ const video = await youtube.getDetails(search.videos[0].id);
|
||||
</p>
|
||||
</details>
|
||||
|
||||
Getting comments:
|
||||
Get comments:
|
||||
|
||||
```js
|
||||
const video = await youtube.getDetails(VIDEO_ID_HERE);
|
||||
@@ -278,7 +278,7 @@ const comments_continuation = await comments.getContinuation();
|
||||
</p>
|
||||
</details>
|
||||
|
||||
Getting subscriptions feed:
|
||||
Get subscriptions feed:
|
||||
```js
|
||||
const mysubfeed = await youtube.getSubscriptionsFeed();
|
||||
```
|
||||
@@ -452,7 +452,7 @@ const mysubfeed = await youtube.getSubscriptionsFeed();
|
||||
</p>
|
||||
</details>
|
||||
|
||||
Getting notifications:
|
||||
Get notifications:
|
||||
|
||||
```js
|
||||
const notifications = await youtube.getNotifications();
|
||||
@@ -493,7 +493,7 @@ const notifications = await youtube.getNotifications();
|
||||
|
||||
---
|
||||
|
||||
* Subscribing/Unsubscribing to channels:
|
||||
* Subscribe/Unsubscribe:
|
||||
```js
|
||||
const video = await youtube.getDetails(VIDEO_ID_HERE); // this is equivalent to opening the watch page on YouTube
|
||||
|
||||
@@ -501,7 +501,7 @@ await video.subscribe();
|
||||
await video.unsubscribe();
|
||||
```
|
||||
|
||||
* Liking/Disliking:
|
||||
* Like/Dislike:
|
||||
```js
|
||||
const video = await youtube.getDetails(VIDEO_ID_HERE); // this is equivalent to opening the watch page on YouTube
|
||||
|
||||
@@ -510,23 +510,23 @@ await video.dislike();
|
||||
await video.removeLike(); // removes either a like or dislike
|
||||
```
|
||||
|
||||
* Commenting:
|
||||
* Comment:
|
||||
```js
|
||||
const video = await youtube.getDetails(VIDEO_ID_HERE);
|
||||
await video.comment('Haha, nice!');
|
||||
```
|
||||
|
||||
* Changing notification preferences:
|
||||
* Change notification preferences:
|
||||
```js
|
||||
const video = await youtube.getDetails(VIDEO_ID_HERE);
|
||||
await video.setNotificationPref('ALL'); // ALL | NONE | PERSONALIZED
|
||||
```
|
||||
|
||||
All of the interactions above will return ```{ success: true, status_code: 200 }``` if everything goes alright.
|
||||
All of the above interactions will return ```{ success: true, status_code: 200 }``` if everything goes alright.
|
||||
|
||||
### Fetching live chats:
|
||||
---
|
||||
YouTube.js isn't able to download live content yet, but it does allow you to fetch live chats in an easy way plus you can also send messages!
|
||||
YouTube.js isn't able to download live content yet, but it does allow you to fetch live chats plus you can also send messages!
|
||||
```js
|
||||
const Innertube = require('youtubei.js');
|
||||
|
||||
@@ -570,7 +570,7 @@ await msg.deleteMessage();
|
||||
### Downloading videos:
|
||||
---
|
||||
|
||||
The library provides an easy-to-use and simple downloader:
|
||||
YouTube.js provides an easy-to-use and simple downloader:
|
||||
|
||||
```js
|
||||
const fs = require('fs');
|
||||
|
||||
@@ -11,11 +11,13 @@ async function start() {
|
||||
console.info('Search results:', search);
|
||||
|
||||
if (search.videos.length === 0)
|
||||
return console.info('[INFO]', 'Could not find any video about that on YouTube.');
|
||||
return console.error('Could not find any video about that on YouTube.');
|
||||
|
||||
const video = await youtube.getDetails(search.videos[0].id);
|
||||
const video = await youtube.getDetails(search.videos[0].id).catch((error) => error);
|
||||
console.info('Video details:', video);
|
||||
if (video.error) return;
|
||||
|
||||
if (video instanceof Error)
|
||||
return console.error('Could not get details for ' + search.videos[0].title);
|
||||
|
||||
if (youtube.logged_in) {
|
||||
const myNotifications = await youtube.getNotifications();
|
||||
|
||||
@@ -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');
|
||||
@@ -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 }]
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -20,7 +20,6 @@ const oauth_reqopts = {
|
||||
'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'
|
||||
}
|
||||
@@ -41,7 +40,7 @@ const default_headers = (session) => {
|
||||
};
|
||||
|
||||
const innertube_request_opts = (info) => {
|
||||
if (info.desktop === undefined) info.desktop = true;
|
||||
info.desktop === undefined && (info.desktop = true);
|
||||
let req_opts = {
|
||||
params: info.params || {},
|
||||
headers: {
|
||||
@@ -58,16 +57,14 @@ const innertube_request_opts = (info) => {
|
||||
'origin': info.desktop ? urls.YT_BASE_URL : urls.YT_MOBILE_URL,
|
||||
}
|
||||
};
|
||||
|
||||
info.id && (req_opts.headers.referer = (info.desktop ? urls.YT_BASE_URL : urls.YT_MOBILE_URL) + '/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;
|
||||
}
|
||||
|
||||
if (info.id) {
|
||||
req_opts.headers.referer = (info.desktop ? urls.YT_BASE_URL : urls.YT_MOBILE_URL) + '/watch?v=' + info.id;
|
||||
}
|
||||
|
||||
return req_opts;
|
||||
};
|
||||
|
||||
@@ -100,9 +97,7 @@ const stream_headers = (range) => {
|
||||
'Referer': urls.YT_BASE_URL,
|
||||
'DNT': '?1'
|
||||
};
|
||||
if (range) {
|
||||
headers.Range = range;
|
||||
}
|
||||
range && (headers.Range = range);
|
||||
return headers;
|
||||
};
|
||||
|
||||
@@ -175,14 +170,13 @@ const formatVideoData = (data, context, desktop) => {
|
||||
return video_details;
|
||||
};
|
||||
|
||||
|
||||
const base64_alphabet = {
|
||||
normal: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'.split(''),
|
||||
reverse: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'.split('')
|
||||
};
|
||||
|
||||
const filters = (order) => {
|
||||
// TODO: Refactor this with protobuf encoding
|
||||
const filters = (order) => {
|
||||
// It seems like all of these are just proto buffers, so I think it'll be easy to refactor
|
||||
switch (order) {
|
||||
case 'any,any,relevance':
|
||||
return 'EgIQAQ%3D%3D';
|
||||
|
||||
146
lib/Innertube.js
146
lib/Innertube.js
@@ -23,30 +23,33 @@ class Innertube extends EventEmitter {
|
||||
|
||||
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';
|
||||
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 {
|
||||
return this.init();
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
} else {
|
||||
this.initialized = false;
|
||||
} catch (err) {
|
||||
return this.init();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -93,43 +96,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_request_opts({ 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);
|
||||
@@ -149,7 +151,7 @@ class Innertube extends EventEmitter {
|
||||
} else {
|
||||
video_data.getLivechat = () => {};
|
||||
}
|
||||
|
||||
|
||||
video_data.like = () => Actions.engage(this, 'like/like', { video_id: id });
|
||||
video_data.dislike = () => Actions.engage(this, 'like/dislike', { video_id: id });
|
||||
video_data.removeLike = () => Actions.engage(this, 'like/removelike', { video_id: id });
|
||||
@@ -161,32 +163,32 @@ class Innertube extends EventEmitter {
|
||||
|
||||
return video_data;
|
||||
}
|
||||
|
||||
|
||||
async getComments(video_id, token) {
|
||||
let comment_section_token;
|
||||
|
||||
|
||||
if (!token) {
|
||||
const data_continuation = await Actions.getContinuation(this, { video_id });
|
||||
const item_section_renderer = data_continuation.data.contents.twoColumnWatchNextResults.results.results.contents.find((item) => item.itemSectionRenderer);
|
||||
comment_section_token = item_section_renderer.itemSectionRenderer.contents[0].continuationItemRenderer.continuationEndpoint.continuationCommand.token;
|
||||
}
|
||||
|
||||
|
||||
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');
|
||||
|
||||
|
||||
let continuation_token;
|
||||
!token && (continuation_token = response.data.onResponseReceivedEndpoints[1].reloadContinuationItemsCommand.continuationItems.find((item) => item.continuationItemRenderer).continuationItemRenderer.continuationEndpoint.continuationCommand.token)
|
||||
|| (continuation_token = response.data.onResponseReceivedEndpoints[0].appendContinuationItemsAction.continuationItems.find((item) => item.continuationItemRenderer).continuationItemRenderer.continuationEndpoint.continuationCommand.token);
|
||||
|
||||
!token && (continuation_token = response.data.onResponseReceivedEndpoints[1].reloadContinuationItemsCommand.continuationItems.find((item) => item.continuationItemRenderer).continuationItemRenderer.continuationEndpoint.continuationCommand.token) ||
|
||||
(continuation_token = response.data.onResponseReceivedEndpoints[0].appendContinuationItemsAction.continuationItems.find((item) => item.continuationItemRenderer).continuationItemRenderer.continuationEndpoint.continuationCommand.token);
|
||||
|
||||
comments_section.getContinuation = () => this.getComments(video_id, continuation_token);
|
||||
|
||||
|
||||
let contents;
|
||||
!token && (contents = response.data.onResponseReceivedEndpoints[1].reloadContinuationItemsCommand.continuationItems)
|
||||
|| (contents = response.data.onResponseReceivedEndpoints[0].appendContinuationItemsAction.continuationItems);
|
||||
|
||||
!token && (contents = response.data.onResponseReceivedEndpoints[1].reloadContinuationItemsCommand.continuationItems) ||
|
||||
(contents = response.data.onResponseReceivedEndpoints[0].appendContinuationItemsAction.continuationItems);
|
||||
|
||||
contents.forEach((thread) => {
|
||||
if (!thread.commentThreadRenderer) return;
|
||||
const comment = {
|
||||
@@ -207,26 +209,26 @@ class Innertube extends EventEmitter {
|
||||
};
|
||||
comments_section.comments.push(comment);
|
||||
});
|
||||
|
||||
|
||||
return comments_section;
|
||||
}
|
||||
|
||||
|
||||
async getSubscriptionsFeed() {
|
||||
const response = await Actions.browse(this, 'subscriptions_feed');
|
||||
if (!response.success) throw new Error('Could not fetch subscriptions feed');
|
||||
|
||||
|
||||
const contents = response.data.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents;
|
||||
const subscriptions_feed = {};
|
||||
|
||||
|
||||
contents.forEach((section) => {
|
||||
if (!section.itemSectionRenderer) return;
|
||||
|
||||
|
||||
const section_contents = section.itemSectionRenderer.contents[0];
|
||||
const section_items = section_contents.shelfRenderer.content.gridRenderer.items;
|
||||
|
||||
|
||||
const key = section_contents.shelfRenderer.title.runs[0].text;
|
||||
subscriptions_feed[key.toLowerCase().replace(/ +/g, '_')] = [];
|
||||
|
||||
|
||||
section_items.forEach((item) => {
|
||||
const content = {
|
||||
title: item.gridVideoRenderer.title.runs.map((run) => run.text).join(' '),
|
||||
@@ -240,18 +242,18 @@ class Innertube extends EventEmitter {
|
||||
owner_badges: item.gridVideoRenderer.ownerBadges && item.gridVideoRenderer.ownerBadges.map((badge) => badge.metadataBadgeRenderer.tooltip) || 'N/A'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
subscriptions_feed[key.toLowerCase().replace(/ +/g, '_')].push(content);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
return subscriptions_feed;
|
||||
}
|
||||
|
||||
async getNotifications() {
|
||||
const response = await Actions.notifications(this, 'get_notification_menu');
|
||||
if (!response.success) throw new Error('Could not fetch notifications');
|
||||
|
||||
|
||||
const contents = response.data.actions[0].openPopupAction.popup.multiPageMenuRenderer.sections[0];
|
||||
if (!contents.multiPageMenuNotificationSectionRenderer) return { error: 'You don\'t have any notification.' };
|
||||
return contents.multiPageMenuNotificationSectionRenderer.items.map((notification) => {
|
||||
@@ -278,11 +280,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_request_opts({ 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_details_reqbody(id, this.sts, this.context)), Constants.innertube_request_opts({ 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 +351,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,7 +362,7 @@ 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 });
|
||||
}
|
||||
|
||||
@@ -7,59 +7,65 @@ 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);
|
||||
|
||||
// Identifies the necessary transformation data 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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 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]]);
|
||||
});
|
||||
} catch (err) {
|
||||
return n;
|
||||
}
|
||||
|
||||
return n_token.join('');
|
||||
}
|
||||
|
||||
getTransformationData() {
|
||||
let transformation_data = '[' + Utils.getStringBetweenStrings(this.raw_code, 'c=[', '];c') + ']';
|
||||
// These variable names have always been the same since earlier player versions, so it should not be a problem for now.
|
||||
let transformation_data = `[${Utils.getStringBetweenStrings(this.raw_code.replace(/\n/g, ''), 'c=[', '];c')}]`;
|
||||
transformation_data = transformation_data
|
||||
.replace(/function\(d,e\)/g, '"function(d,e)')
|
||||
.replace(/function\(d\)/g, '"function(d)')
|
||||
@@ -67,6 +73,8 @@ class NToken {
|
||||
.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])}');
|
||||
@@ -74,11 +82,11 @@ class NToken {
|
||||
return JSON.parse(transformation_data);
|
||||
}
|
||||
|
||||
translateAB(arr, e, is_reverse_base64) {
|
||||
translateAB(arr, index, is_reverse_base64) {
|
||||
let characters = is_reverse_base64 && Constants.base64_alphabet.reverse || Constants.base64_alphabet.normal;
|
||||
arr.forEach(function(char, index, loc) {
|
||||
this.push(loc[index] = characters[(characters.indexOf(char) - characters.indexOf(this[index]) + 64) % characters.length]);
|
||||
}, e.split(''));
|
||||
}, index.split(''));
|
||||
}
|
||||
|
||||
unshiftPop(arr, index) {
|
||||
@@ -97,9 +105,7 @@ class NToken {
|
||||
|
||||
spliceReverseUnshift(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((f) => arr.unshift(f));
|
||||
}
|
||||
|
||||
spliceOnce(arr, index) {
|
||||
|
||||
14
lib/OAuth.js
14
lib/OAuth.js
@@ -28,7 +28,7 @@ class OAuth extends EventEmitter {
|
||||
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();
|
||||
@@ -119,17 +119,17 @@ class OAuth extends EventEmitter {
|
||||
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}`);
|
||||
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 fetch data from auth script: ${response.message}`);
|
||||
if (response instanceof Error) throw new Error(`Could not extract client identify: ${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;
|
||||
const identity_function = Utils.getStringBetweenStrings(response.data, 'YTLR_STORAGE_NAMESPACE",', 'reloadAppFlushLogsMaxTimeoutMs:');
|
||||
const client_identity = identity_function.replace(/\n/g, '').match(this.identity_regex);
|
||||
return client_identity.groups;
|
||||
}
|
||||
|
||||
async refreshAccessToken(refresh_token) {
|
||||
@@ -145,7 +145,7 @@ class OAuth extends EventEmitter {
|
||||
const response = await Axios.post(this.oauth_token_url, JSON.stringify(data), Constants.oauth_reqopts).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'
|
||||
});
|
||||
|
||||
|
||||
@@ -19,10 +19,13 @@ class Player {
|
||||
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);
|
||||
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,7 @@ 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');
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "1.2.1",
|
||||
"version": "1.2.4",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "1.2.1",
|
||||
"version": "1.2.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^0.21.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "1.2.1",
|
||||
"version": "1.2.4",
|
||||
"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": {
|
||||
|
||||
Reference in New Issue
Block a user