Compare commits

..

18 Commits

Author SHA1 Message Date
luan.lrt4@gmail.com
3ec111212c chore(docs): rephrase 2022-03-23 00:45:59 -03:00
luan.lrt4@gmail.com
7ca4b2bb45 chore(release): v1.3.6 2022-03-23 00:43:09 -03:00
luan.lrt4@gmail.com
8d411f25c8 fix: age restricted videos causing uncaught exceptions when logged out 2022-03-23 00:32:51 -03:00
luan.lrt4@gmail.com
80fe969917 refactor: use axios instances to simplify logic & improve code readability 2022-03-22 23:35:39 -03:00
luan.lrt4@gmail.com
13c94fbb8a chore: rephrase comment 2022-03-22 09:36:00 -03:00
luan.lrt4@gmail.com
60ce869054 fix: welp, let's try again 2022-03-22 09:33:08 -03:00
luan.lrt4@gmail.com
1268ac83a6 chore: use optional chaining, bleh 2022-03-22 09:18:52 -03:00
luan.lrt4@gmail.com
5e588d0db5 refactor: use continuation requests for video data 2022-03-22 09:10:25 -03:00
luan.lrt4@gmail.com
8b37bd99b1 chore: add note regarding getVideoInfo() 2022-03-22 05:51:55 -03:00
luan.lrt4@gmail.com
08741de831 fix: oops, wrong param 2022-03-22 05:50:07 -03:00
luan.lrt4@gmail.com
574a595a01 chore: remove unneeded endpoint var 2022-03-22 04:09:32 -03:00
luan.lrt4@gmail.com
16928ee71b chore: update metadata keys 2022-03-21 22:41:38 -03:00
luan.lrt4@gmail.com
de6283080b feat: return comment count in getDetails() 2022-03-21 22:39:41 -03:00
luan.lrt4@gmail.com
23ab8bca4d chore: improve parsing 2022-03-21 19:13:29 -03:00
luan.lrt4@gmail.com
068b86b410 fix: parsing error if streaming data is not available 2022-03-18 17:13:42 -03:00
LuanRT
0b001c0956 fix: getHomeFeed() should work when logged out 2022-03-09 04:10:03 -03:00
LuanRT
4c14662d42 chore(docs): fix typo 2022-03-09 04:07:56 -03:00
LuanRT
f1a9d5d77b chore(docs): fix typo 2022-03-07 19:56:48 -03:00
9 changed files with 397 additions and 355 deletions

100
README.md
View File

@@ -90,9 +90,9 @@ const search = await youtube.search('Interstellar Main Theme', { client: 'YTMUSI
estimated_results: number,
videos: [
{
id: string,
title: string,
description: string,
id: string,
url: string,
metadata:{
view_count: string,
@@ -211,7 +211,7 @@ const suggestions = await youtube.getSearchSuggestions('QUERY', {
</p>
</details>
Get details about a specific video:
Get details about a given video:
```js
const video = await youtube.getDetails('VIDEO_ID');
@@ -246,10 +246,18 @@ const video = await youtube.getDetails('VIDEO_ID');
channel_id: string,
channel_url: string,
external_channel_id: string,
allow_ratings: boolean,
is_live_content: boolean,
is_family_safe: boolean,
is_unlisted: boolean,
is_private: boolean,
is_liked: boolean,
is_disliked: boolean,
is_subscribed: boolean,
subscriber_count: string,
current_notification_preference: string,
likes: { count: number, short_count_text: string },
publish_date_text: string,
has_ypc_metadata: boolean,
category: string,
channel_name: string,
@@ -327,22 +335,29 @@ const homefeed = await youtube.getHomeFeed();
{
id: string,
title: string,
description: string,
channel: string,
metadata: {
view_count: string,
short_view_count_text: { simple_text: string, accessibility_label: string },
thumbnail: {
url: string,
width: number,
height: number
},
moving_thumbnail: {
},
moving_thumbnail: {
url: string,
widt: number,
width: number,
height: number
},
published: string,
badges: [Array],
owner_badges: [Array]
},
published: string,
duration: {
seconds: number,
simple_text: string,
accessibility_label: string
},
badges: string,
owner_badges: [Array]
}
}
// ...
@@ -370,19 +385,21 @@ const mysubsfeed = await youtube.getSubscriptionsFeed();
channel: string,
metadata: {
view_count: string,
short_view_count_text: { simple_text: string, accessibility_label: string },
thumbnail: {
url: string,
width: number,
height: number
url: string,
width: number,
height: number
},
moving_thumbnail: {
url: string,
width: number,
height: number
url: string,
width: number,
height: number
},
published: string,
badges: [Array],
badges: string,
owner_badges: [Array]
}
}
//...
],
@@ -393,42 +410,44 @@ const mysubsfeed = await youtube.getSubscriptionsFeed();
channel: string,
metadata: {
view_count: string,
short_view_count_text: { simple_text: string, accessibility_label: string },
thumbnail: {
url: string,
width: number,
height: number
url: string,
width: number,
height: number
},
moving_thumbnail: {
url: string,
width: number,
height: number
url: string,
width: number,
height: number
},
published: string,
badges: [Array],
badges: string,
owner_badges: [Array]
}
}
//...
],
this_week: [
{
id: string,
title: string,
channel: string,
metadata: {
view_count: string,
thumbnail: {
url: string,
width: number,
height: number
url: string,
width: number,
height: number
},
moving_thumbnail: {
url: string,
width: number,
height: number
url: string,
width: number,
height: number
},
published: string,
badges: [Array],
badges: string,
owner_badges: [Array]
}
}
// ...
]
@@ -453,9 +472,14 @@ const history = await youtube.getHistory();
{
id: string,
title: string,
channel: string,
description: string,
channel: {
name: string,
url: string
},
metadata: {
view_count: string,
short_view_count_text: { simple_text: string, accessibility_label: string },
thumbnail: {
url: string,
width: number,
@@ -467,7 +491,11 @@ const history = await youtube.getHistory();
height: number
},
published: string,
duration: string,
duration: {
seconds: number,
simple_text: string,
accessibility_label: string
},
badges: string,
owner_badges: [Array]
}
@@ -603,7 +631,7 @@ const playlist = await youtube.getPlaylist(search.playlists[0].id);
### Interactions:
---
The library makes it easy to interact with YouTube programatically. However, don't forget that you must be signed in to use the following features!
The library makes it easy to interact with YouTube programmatically. However, don't forget that you must be signed in to use the following features!
* Subscribe/Unsubscribe:
@@ -627,7 +655,7 @@ await youtube.interact.comment('VIDEO_ID', 'Haha, nice video!');
* Change notification preferences:
```js
// Options: ALL | NONE | PERSONALIZED
await youtube.interact.changeNotificationPreferences('VIDEO_ID', 'ALL');
await youtube.interact.changeNotificationPreferences('CHANNEL_ID', 'ALL');
```
These methods will always return ```{ success: true, status_code: 200 }``` if successful.
@@ -942,4 +970,4 @@ All trademarks, logos and brand names are the property of their respective owner
Should you have any questions or concerns please contact me directly via email.
## License
[MIT](https://choosealicense.com/licenses/mit/)
[MIT](https://choosealicense.com/licenses/mit/)

View File

@@ -8,48 +8,36 @@ const Constants = require('./Constants');
/**
* Performs direct interactions on YouTube.
*
* @param {Innertube} session A valid Innertube session.
* @param {string} engagement_type Type of engagement.
* @param {object} args Engagement arguments.
* @returns {Promise.<object>} { success: boolean, status_code: number } | { success: boolean, status_code: number, message: string }
* @param {Innertube} session - A valid Innertube session.
* @param {string} engagement_type - Type of engagement.
* @param {object} args - Engagement arguments.
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
*/
async function engage(session, engagement_type, args = {}) {
if (!session.logged_in) throw new Error('You are not signed-in');
let data;
const data = { context: session.context };
switch (engagement_type) {
case 'like/like':
case 'like/dislike':
case 'like/removelike':
data = {
context: session.context,
target: {
videoId: args.video_id
}
};
data.target = {
videoId: args.video_id
}
break;
case 'subscription/subscribe':
case 'subscription/unsubscribe':
data = {
context: session.context,
channelIds: [args.channel_id],
params: engagement_type == 'subscription/subscribe' ? 'EgIIAhgA' : 'CgIIAhgA'
};
data.channelIds = [args.channel_id];
data.params = engagement_type == 'subscription/subscribe' ? 'EgIIAhgA' : 'CgIIAhgA';
break;
case 'comment/create_comment':
data = {
context: session.context,
commentText: args.text,
createCommentParams: Utils.encodeCommentParams(args.video_id)
};
data.commentText = args.text;
data.createCommentParams = Utils.encodeCommentParams(args.video_id);
break;
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_REQOPTS({ session, id: args.video_id, data })).catch((error) => error);
const response = await session.YTRequester.post(`/${engagement_type}`, JSON.stringify(data));
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
return {
@@ -64,71 +52,48 @@ async function engage(session, engagement_type, args = {}) {
* @param {Innertube} session - A valid Innertube session.
* @param {string} action_type - Type of action.
* @param {object} args - Action argumenets.
* @returns {Promise.<object>} { success: boolean, status_code: number, data: object } | { success: boolean, status_code: number, message: string }
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
*/
async function browse(session, action_type, args = {}) {
if (!session.logged_in && (action_type != 'lyrics' || action_type != 'music_playlist'))
if (!session.logged_in && action_type !== 'lyrics' && action_type !== 'music_playlist')
throw new Error('You are not signed-in');
let data;
const data = { context: session.context };
switch (action_type) {
case 'account_notifications':
data = {
context: session.context,
browseId: 'SPaccount_notifications'
};
data.browseId = 'SPaccount_notifications';
break;
case 'account_privacy':
data = {
context: session.context,
browseId: 'SPaccount_privacy'
};
data.browseId = 'SPaccount_privacy';
break;
case 'history':
data = {
context: session.context,
browseId: 'FEhistory'
}
data.browseId = 'FEhistory';
break;
case 'home_feed':
data = {
context: session.context,
browseId: 'FEwhat_to_watch'
};
data.browseId = 'FEwhat_to_watch';
break;
case 'subscriptions_feed':
data = {
context: session.context,
browseId: 'FEsubscriptions'
};
data.browseId = 'FEsubscriptions';
break;
case 'lyrics':
case 'music_playlist':
const context = JSON.parse(JSON.stringify(session.context)); // deep copy the context obj so we don't accidentally change it
context.client.originalUrl = Constants.URLS.YT_MUSIC_URL;
context.client.originalUrl = Constants.URLS.YT_MUSIC;
context.client.clientVersion = Constants.YTMUSIC_VERSION;
context.client.clientName = 'WEB_REMIX';
data = {
context,
browseId: args.browse_id
}
data.context = context;
data.browseId = args.browse_id;
break;
case 'playlist':
data = {
context: session.context,
browseId: args.browse_id
}
data.browseId = args.browse_id;
break;
default:
}
const client_domain = args.ytmusic && Constants.URLS.YT_MUSIC_URL || Constants.URLS.YT_BASE_URL;
const response = await Axios.post(`${client_domain}/youtubei/v1/browse${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session, ytmusic: args.ytmusic })).catch((error) => error);
const requester = args.ytmusic && session.YTMRequester || session.YTRequester;
const response = await requester.post('/browse', JSON.stringify(data));
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
return {
@@ -145,33 +110,26 @@ async function browse(session, action_type, args = {}) {
* @param {Innertube} session - A valid Innertube session.
* @param {string} action_type - Type of action.
* @param {object} args - Action argumenets.
* @returns {Promise.<object>} { success: boolean, status_code: number, data: object } | { success: boolean, status_code: number, message: string }
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
*/
async function account(session, action_type, args = {}) {
if (!session.logged_in) throw new Error('You are not signed-in');
let data;
const data = {};
switch (action_type) {
case 'account/account_menu':
data = { context: session.context };
data.context = session.context;
break;
case 'account/set_setting':
data = {
context: session.context,
newValue: {
boolValue: args.new_value
},
settingItemId: args.setting_item_id
}
data.context = session.context;
data.newValue = { boolValue: args.new_value };
data.settingItemId = arts.setting_item_id;
break;
default:
break;
}
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 })).catch((error) => error);
const response = await session.YTRequester.post(`/${action_type}`, JSON.stringify(data));
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
return {
@@ -182,18 +140,18 @@ async function account(session, action_type, args = {}) {
}
/**
* Accesses YouTube Music endpoints under /youtubei/v1/music/.
* Accesses YouTube Music endpoints (/youtubei/v1/music/).
*
* @param {Innertube} session - A valid Innertube session.
* @param {string} action_type - Type of action.
* @param {object} args - Action arguments.
* @todo Implement more actions.
* @returns
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
*/
async function music(session, action_type, args) {
const context = JSON.parse(JSON.stringify(session.context)); // deep copy the context obj so we don't accidentally change it
context.client.originalUrl = Constants.URLS.YT_MUSIC_URL;
context.client.originalUrl = Constants.URLS.YT_MUSIC;
context.client.clientVersion = Constants.YTMUSIC_VERSION;
context.client.clientName = 'WEB_REMIX';
@@ -201,18 +159,14 @@ async function music(session, action_type, args) {
switch (action_type) {
case 'get_search_suggestions':
data = {
context,
input: args.input || ''
};
data.context = context;
data.input = args.input || '';
break;
default:
break;
}
const response = await Axios.post(`${Constants.URLS.YT_MUSIC_URL}/youtubei/v1/music/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session, ytmusic: true })).catch((error) => error);
const response = await session.YTMRequester.post(`/music/${action_type}`, JSON.stringify(data));
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
return {
@@ -223,45 +177,38 @@ async function music(session, action_type, args) {
}
/**
* Performs searches on YouTube.
* Searches a given query on YouTube/YTMusic.
*
* @param {Innertube} session - A valid Innertube session.
* @param {string} client - YouTube client: YOUTUBE | YTMUSIC
* @param {object} args - Search arguments.
* @returns {Promise.<object>} - { success: boolean, status_code: number, data: object } | { success: boolean, status_code: number, message: string }
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
*/
async function search(session, client, args = {}) {
if (!args.query) throw new Error('No query was provided');
let data;
const data = { context: session.context };
switch (client) {
case 'YOUTUBE':
data = {
context: session.context,
params: Utils.encodeFilter(args.options.period, args.options.duration, args.options.order),
query: args.query
};
data.params = Utils.encodeFilter(args.options.period, args.options.duration, args.options.order);
data.query = args.query;
break;
case 'YTMUSIC':
const context = JSON.parse(JSON.stringify(session.context)); // deep copy the context obj so we don't accidentally change it
context.client.originalUrl = Constants.URLS.YT_MUSIC_URL;
context.client.originalUrl = Constants.URLS.YT_MUSIC;
context.client.clientVersion = Constants.YTMUSIC_VERSION;
context.client.clientName = 'WEB_REMIX';
data = {
context: context,
query: args.query
};
data.context = context;
data.query = args.query;
break;
default:
break;
}
const response = await Axios.post(`${client === 'YOUTUBE' && Constants.URLS.YT_BASE_URL || Constants.URLS.YT_MUSIC_URL}/youtubei/v1/search${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session, ytmusic: client === 'YTMUSIC' })).catch((error) => error);
const requester = client == 'YOUTUBE' && session.YTRequester || session.YTMRequester;
const response = await requester.post('/search', JSON.stringify(data));
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
return {
@@ -277,38 +224,29 @@ async function search(session, client, args = {}) {
* @param {Innertube} session - A valid Innertube session.
* @param {string} action_type - Type of action.
* @param {object} args - Action arguments.
* @returns {Promise.<object>} - { success: boolean, status_code: number, data: object } | { success: boolean, status_code: number, message: string }
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
*/
async function notifications(session, action_type, args = {}) {
if (!session.logged_in) throw new Error('You are not signed-in');
let data;
const data = {};
switch (action_type) {
case 'modify_channel_preference':
let pref_types = { PERSONALIZED: 1, ALL: 2, NONE: 3 };
data = {
context: session.context,
params: Utils.encodeNotificationPref(args.channel_id, pref_types[args.pref.toUpperCase()])
};
const pref_types = { PERSONALIZED: 1, ALL: 2, NONE: 3 };
data.context = session.context;
data.params = Utils.encodeNotificationPref(args.channel_id, pref_types[args.pref.toUpperCase()]);
break;
case 'get_notification_menu':
data = {
context: session.context,
notificationsMenuRequestType: 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX'
};
data.context = session.context;
data.notificationsMenuRequestType = 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX';
break;
case 'get_unseen_count':
data = {
context: session.context
};
data.context = session.context;
break;
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_REQOPTS({ session })).catch((error) => error);
const response = await session.YTRequester.post(`/notification/${action_type}`, JSON.stringify(data)).catch((err) => err);
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 };
@@ -325,74 +263,54 @@ async function notifications(session, action_type, args = {}) {
* @param {Innertube} session - A valid Innertube session.
* @param {string} action_type - Type of action.
* @param {object} args - Action arguments.
* @returns {Promise.<object>} - { success: boolean, status_code: number, data: object } | { success: boolean, status_code: number, message: string }
* @returns {Promise.<{ success: boolean; data: object; message?: string }>}
*/
async function livechat(session, action_type, args = {}) {
let data;
const data = {};
switch (action_type) {
case 'live_chat/get_live_chat':
data = {
context: session.context,
continuation: args.ctoken
};
data.context = session.context;
data.continuation = args.ctoken;
break;
case 'live_chat/send_message':
data = {
context: session.context,
params: Utils.encodeMessageParams(args.channel_id, args.video_id),
clientMessageId: `ytjs-${Uuid.v4()}`,
richMessage: {
textSegments: [{ text: args.text }]
}
};
data.context = session.context;
data.params = Utils.encodeMessageParams(args.channel_id, args.video_id);
data.clientMessageId = `ytjs-${Uuid.v4()}`;
data.richMessage = {
textSegments: [{ text: args.text }]
}
break;
case 'live_chat/get_item_context_menu':
data = {
context: session.context
};
data.context = session.context;
break;
case 'live_chat/moderate':
data = {
context: session.context,
params: args.cmd_params
};
data.context = session.context;
data.params = args.cmd_params;
break;
case 'updated_metadata':
data = {
context: session.context,
videoId: args.video_id
};
data.context = session.context;
data.videoId = args.video_id;
args.continuation && (data.continuation = args.continuation);
break;
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_REQOPTS({ session, params: args.params })).catch((error) => error);
const response = await session.YTRequester.post(`/${action_type}`, JSON.stringify(data)).catch((err) => err);
if (response instanceof Error) return { success: false, message: response.message };
return { success: true, data: response.data };
}
/**
* Gets detailed data for a video.
* Retrieves video data.
*
* @param {Innertube} session - A valid Innertube session.
* @param {object} args - Request arguments.
* @returns {Promise.<object>} - Video data.
*/
async function getVideoInfo(session, args = {}) {
let response;
!args.desktop &&
(response = await Axios.get(`${Constants.URLS.YT_WATCH_PAGE}?v=${args.id}&t=8s&pbj=1`, Constants.INNERTUBE_REQOPTS({ session, id: args.id, desktop: false })).catch((error) => error)) ||
(response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/player${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
JSON.stringify(Constants.VIDEO_INFO_REQBODY(args.id, session.sts, session.context)), Constants.INNERTUBE_REQOPTS({ session, id: args.id, desktop: true })).catch((error) => error));
const response = await session.YTRequester.post(`/player`, JSON.stringify(Constants.VIDEO_INFO_REQBODY(args.id, session.sts, session.context))).catch((err) => err);
if (response instanceof Error) throw new Error(`Could not get video info: ${response.message}`);
return response.data;
}
@@ -401,7 +319,7 @@ async function getVideoInfo(session, args = {}) {
*
* @param {Innertube} session - A valid Innertube session.
* @param {object} args - Continuation arguments.
* @returns {Promise.<object>} - { success: boolean, status_code: number, data: object } | { success: boolean, status_code: number, message: string }
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
*/
async function getContinuation(session, args = {}) {
let data = { context: session.context };
@@ -412,7 +330,7 @@ async function getContinuation(session, args = {}) {
if (args.ytmusic) {
const context = JSON.parse(JSON.stringify(session.context)); // deep copy the context obj so we don't accidentally change it
context.client.originalUrl = Constants.URLS.YT_MUSIC_URL;
context.client.originalUrl = Constants.URLS.YT_MUSIC;
context.client.clientVersion = Constants.YTMUSIC_VERSION;
context.client.clientName = 'WEB_REMIX';
@@ -430,12 +348,15 @@ async function getContinuation(session, args = {}) {
data.captionsRequested = false;
}
}
const client_domain = args.ytmusic && Constants.URLS.YT_MUSIC_URL || Constants.URLS.YT_BASE_URL;
const response = await Axios.post(`${client_domain}/youtubei/v1/next${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session, ytmusic: args.ytmusic })).catch((error) => error);
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
const requester = args.ytmusic && session.YTMRequester || session.YTRequester;
const response = await requester.post('/next', JSON.stringify(data));
if (response instanceof Error) return {
success: false,
status_code: response.response.status,
message: response.message
};
return {
success: true,
@@ -447,15 +368,19 @@ async function getContinuation(session, args = {}) {
/**
* Gets search suggestions.
*
* @param {Innertube} session
* @param {string} query
* @returns
* @param {Innertube} session - A valid innertube session
* @param {string} query - Search query
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
*/
async function getYTSearchSuggestions(session, query) {
const response = await Axios.get(`${Constants.URLS.YT_SUGGESTIONS}search?client=firefox&ds=yt&q=${query}`,
Constants.DEFAULT_HEADERS(session)).catch((error) => error);
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
if (response instanceof Error) return {
success: false,
status_code: response.response.status,
message: response.message
};
return {
success: true,
@@ -464,4 +389,4 @@ async function getYTSearchSuggestions(session, query) {
};
}
module.exports = { engage, browse, account, music, search, notifications, livechat, getVideoInfo, getContinuation, getYTSearchSuggestions };
module.exports = { engage, browse, account, music, search, notifications, livechat, getVideoInfo, getContinuation, getYTSearchSuggestions };

View File

@@ -4,11 +4,11 @@ const Utils = require('./Utils');
module.exports = {
URLS: {
YT_BASE_URL: 'https://www.youtube.com',
YT_MUSIC_URL: 'https://music.youtube.com',
YT_MOBILE_URL: 'https://m.youtube.com',
YT_WATCH_PAGE: 'https://m.youtube.com/watch',
YT_SUGGESTIONS: 'https://suggestqueries.google.com/complete/'
YT_BASE: 'https://www.youtube.com',
YT_BASE_API: 'https://www.youtube.com/youtubei/',
YT_SUGGESTIONS: 'https://suggestqueries.google.com/complete/',
YT_MUSIC: 'https://music.youtube.com',
YT_MUSIC_BASE_API: 'https://music.youtube.com/youtubei/'
},
OAUTH: {
SCOPE: 'http://gdata.youtube.com https://www.googleapis.com/auth/youtube-paid-content',
@@ -33,9 +33,9 @@ module.exports = {
'Referer': 'https://www.google.com/',
'Accept': 'text/html',
'Accept-Language': 'en-US,en',
'Accept-Encoding': 'gzip',
'Upgrade-Insecure-Requests': 1
}
'Accept-Encoding': 'gzip'
},
};
},
STREAM_HEADERS: {
@@ -46,36 +46,30 @@ module.exports = {
'Referer': 'https://www.youtube.com',
'DNT': '?1'
},
INNERTUBE_REQOPTS: (info) => {
info.desktop === undefined && (info.desktop = true);
const origin = info.ytmusic && 'https://music.youtube.com' ||
info.desktop && 'https://www.youtube.com' || 'https://m.youtube.com';
INNERTUBE_HEADERS: (info) => {
const origin = info.ytmusic && 'https://music.youtube.com' || 'https://www.youtube.com';
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': origin,
'origin': origin,
}
const headers = {
'accept': '*/*',
'user-agent': Utils.getRandomUserAgent('desktop').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': 1,
'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': origin,
'origin': origin
};
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;
if (info.session.logged_in) {
headers.Cookie = info.session.cookie;
headers.authorization = info.session.cookie.length && info.session.auth_apisid || `Bearer ${info.session.access_token}`;
}
return req_opts;
return headers
},
VIDEO_INFO_REQBODY: (id, sts, context) => {
return {
@@ -98,7 +92,7 @@ module.exports = {
},
YTMUSIC_VERSION: '1.20211213.00.00',
METADATA_KEYS: [
'embed', 'view_count', 'average_rating',
'embed', 'view_count', 'average_rating', 'allow_ratings',
'length_seconds', 'channel_id', 'channel_url',
'external_channel_id', 'is_live_content', 'is_family_safe',
'is_unlisted', 'is_private', 'has_ypc_metadata',
@@ -108,7 +102,7 @@ module.exports = {
],
BLACKLISTED_KEYS: [
'is_owner_viewing', 'is_unplugged_corpus',
'is_crawlable', 'allow_ratings', 'author'
'is_crawlable', 'author'
],
ACCOUNT_SETTINGS: {
// Notifications

View File

@@ -34,13 +34,14 @@ class Innertube {
}
async #init() {
const response = await Axios.get(Constants.URLS.YT_BASE_URL, Constants.DEFAULT_HEADERS(this)).catch((error) => error);
const response = await Axios.get(Constants.URLS.YT_BASE, Constants.DEFAULT_HEADERS(this)).catch((error) => error);
if (response instanceof Error) throw new Error(`Could not retrieve Innertube session: ${response.message}`);
try {
const data = JSON.parse(`{${Utils.getStringBetweenStrings(response.data, 'ytcfg.set({', '});')}}`);
if (data.INNERTUBE_CONTEXT) {
this.key = data.INNERTUBE_API_KEY;
this.key = data.INNERTUBE_API_KEY;
this.version = data.INNERTUBE_API_VERSION;
this.context = data.INNERTUBE_CONTEXT;
this.player_url = data.PLAYER_JS_URL;
@@ -51,20 +52,35 @@ class Innertube {
this.context.client.gl = 'US';
/**
* @event auth - Fired when signing in to an account.
* @event update-credentials - Fired when the access token is no longer valid.
* @event Innertube#auth - Fired when signing in to an account.
* @event Innertube#update-credentials - Fired when the access token is no longer valid.
* @type {EventEmitter}
*/
this.ev = new EventEmitter();
this.#player = new Player(this);
await this.#player.init();
if (this.logged_in && this.cookie.length > 1) {
if (this.logged_in && this.cookie.length) {
this.auth_apisid = Utils.getStringBetweenStrings(this.cookie, 'PAPISID=', ';');
this.auth_apisid = Utils.generateSidAuth(this.auth_apisid);
}
// Axios instances
this.YTRequester = Axios.create({
baseURL: Constants.URLS.YT_BASE_API + this.version,
timeout: 15000,
headers: Constants.INNERTUBE_HEADERS({ session: this, ytmusic: false }),
params: { key: this.key }
});
this.YTMRequester = Axios.create({
baseURL: Constants.URLS.YT_MUSIC_BASE_API + this.version,
timeout: 15000,
headers: Constants.INNERTUBE_HEADERS({ session: this, ytmusic: true }),
params: { key: this.key }
});
this.#initMethods();
} else {
throw new Error('Could not retrieve Innertube session due to unknown reasons');
@@ -262,8 +278,16 @@ class Innertube {
this.access_token = auth_info.access_token;
this.refresh_token = auth_info.refresh_token;
this.logged_in = true;
// API key is not needed if logged in via OAuth
delete this.YTRequester.defaults.params.key;
delete this.YTMRequester.defaults.params.key;
// Update default headers
this.YTRequester.defaults.headers = Constants.INNERTUBE_HEADERS({ session: this, ytmusic: false });
this.YTMRequester.defaults.headers = Constants.INNERTUBE_HEADERS({ session: this, ytmusic: true });
resolve();
resolve();
} else {
oauth.on('auth', (data) => {
if (data.status === 'SUCCESS') {
@@ -271,6 +295,13 @@ class Innertube {
this.access_token = data.credentials.access_token;
this.refresh_token = data.credentials.refresh_token;
this.logged_in = true;
delete this.YTRequester.defaults.params.key;
delete this.YTMRequester.defaults.params.key;
this.YTRequester.defaults.headers = Constants.INNERTUBE_HEADERS({ session: this, ytmusic: false });
this.YTMRequester.defaults.headers = Constants.INNERTUBE_HEADERS({ session: this, ytmusic: true });
resolve();
} else {
this.ev.emit('auth', data);
@@ -373,30 +404,34 @@ class Innertube {
async getDetails(video_id) {
if (!video_id) throw new Error('You must provide a video id');
const data = await Actions.getVideoInfo(this, { id: video_id, is_desktop: false });
const refined_data = new Parser(this, data, { client: 'YOUTUBE', data_type: 'VIDEO_INFO', desktop_v: false }).parse();
const data = await Actions.getVideoInfo(this, { id: video_id });
const continuation = await Actions.getContinuation(this, { video_id });
data.continuation = continuation.data;
const details = new Parser(this, data, { client: 'YOUTUBE', data_type: 'VIDEO_INFO' }).parse();
if (refined_data.metadata.is_live_content) {
if (details.metadata.is_live_content) {
const data_continuation = await Actions.getContinuation(this, { video_id });
if (data_continuation.data.contents.twoColumnWatchNextResults.conversationBar) {
refined_data.getLivechat = () => new Livechat(this, data_continuation.data.contents.twoColumnWatchNextResults.conversationBar.liveChatRenderer.continuations[0].reloadContinuationData.continuation, refined_data.metadata.channel_id, video_id);
details.getLivechat = () => new Livechat(this, data_continuation.data.contents.twoColumnWatchNextResults.conversationBar.liveChatRenderer.continuations[0].reloadContinuationData.continuation, details.metadata.channel_id, video_id);
} else {
refined_data.getLivechat = () => { };
details.getLivechat = () => { };
}
} else {
refined_data.getLivechat = () => { };
details.getLivechat = () => { };
}
// Functions
details.like = () => Actions.engage(this, 'like/like', { video_id });
details.dislike = () => Actions.engage(this, 'like/dislike', { video_id });
details.removeLike = () => Actions.engage(this, 'like/removelike', { video_id });
details.subscribe = () => Actions.engage(this, 'subscription/subscribe', { channel_id: details.metadata.channel_id });
details.unsubscribe = () => Actions.engage(this, 'subscription/unsubscribe', { channel_id: details.metadata.channel_id });
details.comment = (text) => Actions.engage(this, 'comment/create_comment', { video_id, text });
details.getComments = () => this.getComments(video_id);
details.changeNotificationPreferences = (type) => Actions.notifications(this, 'modify_channel_preference', { channel_id: details.metadata.channel_id, pref: type || 'NONE' });
refined_data.like = () => Actions.engage(this, 'like/like', { video_id });
refined_data.dislike = () => Actions.engage(this, 'like/dislike', { video_id });
refined_data.removeLike = () => Actions.engage(this, 'like/removelike', { video_id });
refined_data.subscribe = () => Actions.engage(this, 'subscription/subscribe', { channel_id: refined_data.metadata.channel_id });
refined_data.unsubscribe = () => Actions.engage(this, 'subscription/unsubscribe', { channel_id: refined_data.metadata.channel_id });
refined_data.comment = (text) => Actions.engage(this, 'comment/create_comment', { video_id, text });
refined_data.getComments = () => this.getComments(video_id);
refined_data.changeNotificationPreferences = (type) => Actions.notifications(this, 'modify_channel_preference', { channel_id: refined_data.metadata.channel_id, pref: type || 'NONE' });
return refined_data;
return details;
}
/**
@@ -508,15 +543,27 @@ class Innertube {
const content = {
id: item.videoRenderer.videoId,
title: item.videoRenderer.title.runs.map((run) => run.text).join(' '),
channel: item.videoRenderer.shortBylineText && item.videoRenderer.shortBylineText.runs[0].text || 'N/A',
description: item.videoRenderer.descriptionSnippet && item.videoRenderer.descriptionSnippet.runs[0].text || 'N/A',
channel: {
name: item.videoRenderer.shortBylineText && item.videoRenderer.shortBylineText.runs[0].text || 'N/A',
url: item.videoRenderer.shortBylineText && `${Constants.URLS.YT_BASE}${item.videoRenderer.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}` || 'N/A',
},
metadata: {
view_count: item.videoRenderer.viewCountText && item.videoRenderer.viewCountText.simpleText || 'N/A',
short_view_count_text: {
simple_text: item.videoRenderer.shortViewCountText && item.videoRenderer.shortViewCountText.simpleText || 'N/A',
accessibility_label: item.videoRenderer.shortViewCountText && (item.videoRenderer.shortViewCountText.accessibility && item.videoRenderer.shortViewCountText.accessibility.accessibilityData.label || 'N/A') || 'N/A',
},
thumbnail: item.videoRenderer.thumbnail && item.videoRenderer.thumbnail.thumbnails.slice(-1)[0] || [],
moving_thumbnail: item.videoRenderer.richThumbnail && item.videoRenderer.richThumbnail.movingThumbnailRenderer.movingThumbnailDetails.thumbnails[0] || [],
published: item.videoRenderer.publishedTimeText && item.videoRenderer.publishedTimeText.simpleText || 'N/A',
duration: item.videoRenderer.lengthText && item.videoRenderer.lengthText.simpleText || 'N/A',
badges: item.videoRenderer.badges && item.videoRenderer.badges.map((badge) => badge.metadataBadgeRenderer.label) || 'N/A',
owner_badges: item.videoRenderer.ownerBadges && item.videoRenderer.ownerBadges.map((badge) => badge.metadataBadgeRenderer.tooltip) || 'N/A'
duration: {
seconds: Utils.timeToSeconds(item.videoRenderer.lengthText && item.videoRenderer.lengthText.simpleText || '0'),
simple_text: item.videoRenderer.lengthText && item.videoRenderer.lengthText.simpleText || 'N/A',
accessibility_label: item.videoRenderer.lengthText && item.videoRenderer.lengthText.accessibility.accessibilityData.label || 'N/A'
},
badges: item.videoRenderer.badges && item.videoRenderer.badges.map((badge) => badge.metadataBadgeRenderer.label) || [],
owner_badges: item.videoRenderer.ownerBadges && item.videoRenderer.ownerBadges.map((badge) => badge.metadataBadgeRenderer.tooltip) || []
}
};
history.push(content);
@@ -527,7 +574,7 @@ class Innertube {
}
/**
* Returns YouTube's home feed.
* Returns YouTube's home feed (aka recommendations).
* @returns {Promise.<[{ id: string; title: string; channel: string; metadata: {} }]>}
*/
async getHomeFeed() {
@@ -535,7 +582,7 @@ class Innertube {
if (!response.success) throw new Error('Could not get home feed');
const contents = response.data.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.richGridRenderer.contents;
return contents.map((item) => {
const content = item.richItemRenderer && item.richItemRenderer.content.videoRenderer &&
item.richItemRenderer.content || undefined;
@@ -543,12 +590,25 @@ class Innertube {
if (content) return {
id: content.videoRenderer.videoId,
title: content.videoRenderer.title.runs.map((run) => run.text).join(' '),
channel: content.videoRenderer.shortBylineText && content.videoRenderer.shortBylineText.runs[0].text || 'N/A',
description: content.videoRenderer.descriptionSnippet && content.videoRenderer.descriptionSnippet.runs[0].text || 'N/A',
channel: {
name: content.videoRenderer.shortBylineText && content.videoRenderer.shortBylineText.runs[0].text || 'N/A',
url: content.videoRenderer.shortBylineText && `${Constants.URLS.YT_BASE}${content.videoRenderer.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}` || 'N/A',
},
metadata: {
view_count: content.videoRenderer.viewCountText && content.videoRenderer.viewCountText.simpleText || 'N/A',
thumbnail: content.videoRenderer.thumbnail && content.videoRenderer.thumbnail.thumbnails.slice(-1)[0] || [],
moving_thumbnail: content.videoRenderer.richThumbnail && content.videoRenderer.richThumbnail.movingThumbnailRenderer.movingThumbnailDetails.thumbnails[0] || [],
short_view_count_text: {
simple_text: content.videoRenderer.shortViewCountText && content.videoRenderer.shortViewCountText.simpleText || 'N/A',
accessibility_label: content.videoRenderer.shortViewCountText && (content.videoRenderer.shortViewCountText.accessibility && content.videoRenderer.shortViewCountText.accessibility.accessibilityData.label || 'N/A') || 'N/A',
},
thumbnail: content.videoRenderer.thumbnail && content.videoRenderer.thumbnail.thumbnails.slice(-1)[0] || {},
moving_thumbnail: content.videoRenderer.richThumbnail && content.videoRenderer.richThumbnail.movingThumbnailRenderer.movingThumbnailDetails.thumbnails[0] || {},
published: content.videoRenderer.publishedTimeText && content.videoRenderer.publishedTimeText.simpleText || 'N/A',
duration: {
seconds: Utils.timeToSeconds(content.videoRenderer.lengthText && content.videoRenderer.lengthText.simpleText || '0'),
simple_text: content.videoRenderer.lengthText && content.videoRenderer.lengthText.simpleText || 'N/A',
accessibility_label: content.videoRenderer.lengthText && content.videoRenderer.lengthText.accessibility.accessibilityData.label || 'N/A'
},
badges: content.videoRenderer.badges && content.videoRenderer.badges.map((badge) => badge.metadataBadgeRenderer.label) || [],
owner_badges: content.videoRenderer.ownerBadges && content.videoRenderer.ownerBadges.map((badge) => badge.metadataBadgeRenderer.tooltip) || []
}
@@ -580,13 +640,21 @@ class Innertube {
const content = {
id: item.gridVideoRenderer.videoId,
title: item.gridVideoRenderer.title.runs.map((run) => run.text).join(' '),
channel: item.gridVideoRenderer.shortBylineText && item.gridVideoRenderer.shortBylineText.runs[0].text || 'N/A',
channel: {
name: item.gridVideoRenderer.shortBylineText && item.gridVideoRenderer.shortBylineText.runs[0].text || 'N/A',
url: item.gridVideoRenderer.shortBylineText && `${Constants.URLS.YT_BASE}${item.gridVideoRenderer.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}` || 'N/A',
},
metadata: {
view_count: item.gridVideoRenderer.viewCountText && item.gridVideoRenderer.viewCountText.simpleText || 'N/A',
short_view_count_text: {
simple_text: item.gridVideoRenderer.shortViewCountText && item.gridVideoRenderer.shortViewCountText.simpleText || 'N/A',
accessibility_label: item.gridVideoRenderer.shortViewCountText && (item.gridVideoRenderer.shortViewCountText.accessibility && item.gridVideoRenderer.shortViewCountText.accessibility.accessibilityData.label || 'N/A') || 'N/A',
},
thumbnail: item.gridVideoRenderer.thumbnail && item.gridVideoRenderer.thumbnail.thumbnails.slice(-1)[0] || [],
moving_thumbnail: item.gridVideoRenderer.richThumbnail && item.gridVideoRenderer.richThumbnail.movingThumbnailRenderer.movingThumbnailDetails.thumbnails[0] || {},
published: item.gridVideoRenderer.publishedTimeText && item.gridVideoRenderer.publishedTimeText.simpleText || 'N/A',
badges: item.gridVideoRenderer.badges && item.gridVideoRenderer.badges.map((badge) => badge.metadataBadgeRenderer.label) || 'N/A',
owner_badges: item.gridVideoRenderer.ownerBadges && item.gridVideoRenderer.ownerBadges.map((badge) => badge.metadataBadgeRenderer.tooltip) || 'N/A'
badges: item.gridVideoRenderer.badges && item.gridVideoRenderer.badges.map((badge) => badge.metadataBadgeRenderer.label) || [],
owner_badges: item.gridVideoRenderer.ownerBadges && item.gridVideoRenderer.ownerBadges.map((badge) => badge.metadataBadgeRenderer.tooltip) || []
}
};
@@ -718,7 +786,7 @@ class Innertube {
options.type = options.type || 'videoandaudio';
options.format = options.format || 'mp4';
const data = await Actions.getVideoInfo(this, { id, desktop: true });
const data = await Actions.getVideoInfo(this, { id });
const streaming_data = this.#chooseFormat(options, data);
if (!streaming_data.selected_format) throw new Error('Could not find any suitable format.');
@@ -746,7 +814,7 @@ class Innertube {
let cancelled = false;
const stream = new Stream.PassThrough();
Actions.getVideoInfo(this, { id, desktop: true }).then(async (video_data) => {
Actions.getVideoInfo(this, { id }).then(async (video_data) => {
if (video_data.playabilityStatus.status === 'LOGIN_REQUIRED')
return stream.emit('error', { message: 'You must login to download age-restricted videos.', error_type: 'LOGIN_REQUIRED', playability_status: video_data.playabilityStatus.status });
if (!video_data.streamingData)
@@ -875,4 +943,4 @@ class Innertube {
}
}
module.exports = Innertube;
module.exports = Innertube;

View File

@@ -11,10 +11,9 @@ class OAuth extends EventEmitter {
this.auth_info = auth_info;
this.refresh_interval = 5;
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.guide_url = `${Constants.URLS.YT_BASE_URL}/youtubei/v1/guide`;
this.oauth_code_url = `${Constants.URLS.YT_BASE}/o/oauth2/device/code`;
this.oauth_token_url = `${Constants.URLS.YT_BASE}/o/oauth2/token`;
this.model_name = Constants.OAUTH.MODEL_NAME;
this.grant_type = Constants.OAUTH.GRANT_TYPE;
this.scope = Constants.OAUTH.SCOPE;
@@ -124,7 +123,7 @@ class OAuth extends EventEmitter {
/**
* Gets a new access token using a refresh token.
* @returns {object.<{ credentials: { access_token: string; refresh_token: string; expires: Date }; status: string }>}
* @returns {Promise.<{ credentials: { access_token: string; refresh_token: string; expires: Date }; status: string }>}
*/
async refreshAccessToken() {
const identity = await this.#getClientIdentity();
@@ -172,13 +171,13 @@ class OAuth extends EventEmitter {
*/
async #getClientIdentity() {
// This 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);
const yttv_response = await Axios.get(`${Constants.URLS.YT_BASE}/tv`, Constants.OAUTH.HEADERS).catch((error) => error);
if (yttv_response instanceof Error) throw new Error(`Could not extract client identity: ${yttv_response.message}`);
// Here we download 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 script_url = `${Constants.URLS.YT_BASE}/${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 identity: ${response.message}`);
@@ -197,4 +196,4 @@ class OAuth extends EventEmitter {
}
}
module.exports = OAuth;
module.exports = OAuth;

View File

@@ -45,12 +45,12 @@ class Parser {
if (!data.videoRenderer) return;
const video = data.videoRenderer;
return {
id: video.videoId,
title: video.title.runs[0].text,
description: video.detailedMetadataSnippets && video.detailedMetadataSnippets[0].snippetText.runs.map((item) => item.text).join('') || 'N/A',
channel_url: `${Constants.URLS.YT_BASE}${video.ownerText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}`,
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}`,
metadata: {
view_count: video.viewCountText && video.viewCountText.simpleText || 'N/A',
short_view_count_text: {
@@ -207,25 +207,20 @@ class Parser {
items
}
}
/**
* Video data is parsed dynamically, so if youtube decides to add something new we won't have to change anything here.
*/
#parseVideoInfo() {
const desktop_v = this.args.desktop_v;
const playability_status = desktop_v && this.data.playabilityStatus ||
this.data[2].playerResponse.playabilityStatus;
const playability_status = this.data.playabilityStatus;
if (playability_status.status == 'ERROR')
throw new Error(`Could not retrieve details for this video: ${playability_status.status} - ${playability_status.reason}`);
const details = desktop_v && this.data.videoDetails ||
this.data[2].playerResponse.videoDetails;
const microformat = desktop_v && this.data.microformat.playerMicroformatRenderer ||
this.data[2].playerResponse.microformat.playerMicroformatRenderer;
const streaming_data = desktop_v && this.data.streamingData ||
this.data[2].playerResponse.streamingData;
throw new Error(`Could not retrieve video details: ${playability_status.status} - ${playability_status.reason}`);
const details = this.data.videoDetails;
const microformat = this.data.microformat.playerMicroformatRenderer;
const streaming_data = this.data.streamingData;
const response = {
id: '',
title: '',
@@ -236,7 +231,8 @@ class Parser {
const mf_raw_data = Object.entries(microformat);
const dt_raw_data = Object.entries(details);
// Extracts most of the metadata
mf_raw_data.forEach((entry) => {
const key = Utils.camelToSnake(entry[0]);
if (Constants.METADATA_KEYS.includes(key)) {
@@ -248,7 +244,8 @@ class Parser {
response[key] = entry[1];
}
});
// Extracts extra details
dt_raw_data.forEach((entry) => {
const key = Utils.camelToSnake(entry[0]);
if (Constants.BLACKLISTED_KEYS.includes(key)) return;
@@ -262,26 +259,54 @@ class Parser {
(response[key] = entry[1]);
}
});
if (!desktop_v) {
const dislike_available = this.data[3].response.contents.singleColumnWatchNextResults.results.results.contents[1]
.slimVideoMetadataSectionRenderer.contents[1].slimVideoActionBarRenderer.buttons[1].slimMetadataToggleButtonRenderer
.button.toggleButtonRenderer.defaultText.accessibility && true || false;
response.metadata.likes = parseInt(this.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, ''));
response.metadata.dislikes = dislike_available && parseInt(this.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, '')) || 0;
// Data continuation is only required in getDetails()
if (this.data.continuation) {
const primary_info_renderer = this.data.continuation.contents.twoColumnWatchNextResults
.results.results.contents.find((item) => item.videoPrimaryInfoRenderer).videoPrimaryInfoRenderer;
const secondary_info_renderer = this.data.continuation.contents.twoColumnWatchNextResults
.results.results.contents.find((item) => item.videoSecondaryInfoRenderer).videoSecondaryInfoRenderer;
const like_btn = primary_info_renderer.videoActions.menuRenderer
.topLevelButtons.find((item) => item.toggleButtonRenderer.defaultIcon.iconType == 'LIKE');
const dislike_btn = primary_info_renderer.videoActions.menuRenderer
.topLevelButtons.find((item) => item.toggleButtonRenderer.defaultIcon.iconType == 'DISLIKE');
const notification_toggle_btn = secondary_info_renderer.subscribeButton.subscribeButtonRenderer
?.notificationPreferenceButton?.subscriptionNotificationToggleButtonRenderer;
// These will always be false if logged out.
response.metadata.is_liked = like_btn.toggleButtonRenderer.isToggled;
response.metadata.is_disliked = dislike_btn.toggleButtonRenderer.isToggled;
response.metadata.is_subscribed = secondary_info_renderer.subscribeButton.subscribeButtonRenderer?.subscribed || false;
response.metadata.subscriber_count = secondary_info_renderer.owner.videoOwnerRenderer?.subscriberCountText?.simpleText || 'N/A';
response.metadata.current_notification_preference = notification_toggle_btn?.states.find((state) => state.stateId == notification_toggle_btn.currentStateId)
.state.buttonRenderer.icon.iconType || 'N/A';
// Simpler version of publish_date
response.metadata.publish_date_text = primary_info_renderer.dateText.simpleText;
// Only parse like count if it's enabled
if (response.metadata.allow_ratings) {
response.metadata.likes = {
count: parseInt(like_btn.toggleButtonRenderer.defaultText.accessibility.accessibilityData.label.replace(/\D/g, '')),
short_count_text: like_btn.toggleButtonRenderer.defaultText.simpleText
};
}
response.metadata.owner_badges = secondary_info_renderer.owner.videoOwnerRenderer?.badges?.map((badge) => badge.metadataBadgeRenderer.tooltip) || [];
}
response.metadata.available_qualities = [...new Set(streaming_data.adaptiveFormats.filter(v => v.qualityLabel)
.map(v => v.qualityLabel).sort((a, b) => + a.replace(/\D/gi, '') - + b.replace(/\D/gi, '')))];
streaming_data && streaming_data.adaptiveFormats &&
(response.metadata.available_qualities = [...new Set(streaming_data.adaptiveFormats.filter(v => v.qualityLabel)
.map(v => v.qualityLabel).sort((a, b) => +a.replace(/\D/gi, '') - +b.replace(/\D/gi, '')))]) ||
(response.metadata.available_qualities = []);
return response;
}
}
module.exports = Parser;
module.exports = Parser;

View File

@@ -18,7 +18,7 @@ class Player {
this.sig_decipher_sc = this.#getSigDecipherCode(player_data);
this.ntoken_sc = 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);
const response = await Axios.get(`${Constants.URLS.YT_BASE}${this.session.player_url}`, { path: this.session.playerUrl, headers: { 'content-type': 'text/javascript', 'user-agent': Utils.getRandomUserAgent('desktop').userAgent } }).catch((error) => error);
if (response instanceof Error) throw new Error('Could not download player script: ' + response.message);
try {

31
package-lock.json generated
View File

@@ -1,18 +1,21 @@
{
"name": "youtubei.js",
"version": "1.3.6",
"version": "1.3.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "youtubei.js",
"version": "1.3.6",
"version": "1.3.7",
"license": "MIT",
"dependencies": {
"axios": "^0.21.4",
"protons": "^2.0.3",
"user-agents": "^1.0.778",
"uuid": "^8.3.2"
},
"funding": {
"url": "https://ko-fi.com/luanrt"
}
},
"node_modules/axios": {
@@ -53,9 +56,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==",
"version": "1.14.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
"funding": [
{
"type": "individual",
@@ -127,9 +130,9 @@
}
},
"node_modules/user-agents": {
"version": "1.0.943",
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.943.tgz",
"integrity": "sha512-0qfnGXlAO9jbfiFAnhKG3CIWxyfr7s5WgwVlV7ns2jnl8BwQI0rTJ4bdx0HR7FasInT9TCvsulmZgLgBQHbkZA==",
"version": "1.0.963",
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.963.tgz",
"integrity": "sha512-TzX6aH2dfEbPsTcQk1ZKYCKXU9VUIZy8Vyaiml2pdraKRB1TC6hcPx38M5JLGZicNXyuVInsbSW+xQ30etZmyw==",
"dependencies": {
"dot-json": "^1.2.2",
"lodash.clonedeep": "^4.5.0"
@@ -179,9 +182,9 @@
}
},
"follow-redirects": {
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
"version": "1.14.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
},
"lodash.clonedeep": {
"version": "4.5.0",
@@ -239,9 +242,9 @@
}
},
"user-agents": {
"version": "1.0.943",
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.943.tgz",
"integrity": "sha512-0qfnGXlAO9jbfiFAnhKG3CIWxyfr7s5WgwVlV7ns2jnl8BwQI0rTJ4bdx0HR7FasInT9TCvsulmZgLgBQHbkZA==",
"version": "1.0.963",
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.963.tgz",
"integrity": "sha512-TzX6aH2dfEbPsTcQk1ZKYCKXU9VUIZy8Vyaiml2pdraKRB1TC6hcPx38M5JLGZicNXyuVInsbSW+xQ30etZmyw==",
"requires": {
"dot-json": "^1.2.2",
"lodash.clonedeep": "^4.5.0"

View File

@@ -1,6 +1,6 @@
{
"name": "youtubei.js",
"version": "1.3.6",
"version": "1.3.7",
"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": {