mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-25 15:52:13 +00:00
feat: add support for music search
This commit is contained in:
165
lib/Actions.js
165
lib/Actions.js
@@ -5,9 +5,17 @@ const Axios = require('axios');
|
||||
const Utils = require('./Utils');
|
||||
const Constants = require('./Constants');
|
||||
|
||||
/**
|
||||
* Performs direct interactions on YouTube.
|
||||
*
|
||||
* @param {object} session A valid Innertube session.
|
||||
* @param {string} engagement_type Type of engagement.
|
||||
* @param {object} args Engagement arguments.
|
||||
* @returns {object} { success: boolean, status_code: number } | { success: boolean, status_code: number, message: string }
|
||||
*/
|
||||
async function engage(session, engagement_type, args = {}) {
|
||||
if (!session.logged_in) throw new Error('You are not signed-in');
|
||||
|
||||
|
||||
let data;
|
||||
switch (engagement_type) {
|
||||
case 'like/like':
|
||||
@@ -31,23 +39,30 @@ async function engage(session, engagement_type, args = {}) {
|
||||
data = {
|
||||
context: session.context,
|
||||
commentText: args.text,
|
||||
createCommentParams: Utils.generateCommentParams(args.video_id)
|
||||
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 Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/${engagement_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
|
||||
JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session, id: args.video_id, data })).catch((error) => error);
|
||||
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status_code: response.status
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Accesses YouTube's various sections.
|
||||
*
|
||||
* @param {object} session A valid Innertube session.
|
||||
* @param {string} action_type Type of action.
|
||||
* @returns {object} { success: boolean, status_code: number, data: object } | { success: boolean, status_code: number, message: string }
|
||||
*/
|
||||
async function browse(session, action_type) {
|
||||
if (!session.logged_in) throw new Error('You are not signed-in');
|
||||
|
||||
@@ -62,11 +77,11 @@ async function browse(session, action_type) {
|
||||
default:
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/browse${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
|
||||
JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session })).catch((error) => error);
|
||||
|
||||
const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/browse${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
|
||||
JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session })).catch((error) => error);
|
||||
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status_code: response.status,
|
||||
@@ -74,17 +89,46 @@ async function browse(session, action_type) {
|
||||
};
|
||||
}
|
||||
|
||||
async function search(session, args = {}) {
|
||||
/**
|
||||
* Performs searches on YouTube.
|
||||
*
|
||||
* @param {object} session A valid Innertube session.
|
||||
* @param {string} client YouTube client: YOUTUBE | YTMUSIC
|
||||
* @param {object} args Search arguments.
|
||||
* @returns {object} { success: boolean, status_code: number, data: object } | { success: boolean, status_code: number, message: string }
|
||||
*/
|
||||
async function search(session, client, args = {}) {
|
||||
if (!args.query) throw new Error('No query was provided');
|
||||
|
||||
const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/search${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
|
||||
JSON.stringify({
|
||||
context: session.context,
|
||||
params: Utils.encodeFilter(args.options.period, args.options.duration, args.options.order),
|
||||
query: args.query
|
||||
}), Constants.INNERTUBE_REQOPTS({ session })).catch((error) => error);
|
||||
let data;
|
||||
switch (client) {
|
||||
case 'YOUTUBE':
|
||||
data = {
|
||||
context: session.context,
|
||||
params: Utils.encodeFilter(args.options.period, args.options.duration, args.options.order),
|
||||
query: args.query
|
||||
};
|
||||
break;
|
||||
case 'YTMUSIC':
|
||||
const yt_music_context = JSON.parse(JSON.stringify(session.context)); // deep copy the context obj so we don't accidentally change it
|
||||
|
||||
yt_music_context.client.originalUrl = Constants.URLS.YT_MUSIC_URL;
|
||||
yt_music_context.client.clientVersion = '1.20211213.00.00';
|
||||
yt_music_context.client.clientName = 'WEB_REMIX';
|
||||
|
||||
data = {
|
||||
context: yt_music_context,
|
||||
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);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status_code: response.status,
|
||||
@@ -92,9 +136,18 @@ async function search(session, args = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Interacts with YouTube's notification system.
|
||||
*
|
||||
* @param {object} session A valid Innertube session.
|
||||
* @param {string} action_type Type of action.
|
||||
* @param {object} args Action arguments.
|
||||
* @returns {object} { success: boolean, status_code: number, data: object } | { success: boolean, status_code: number, message: string }
|
||||
*/
|
||||
async function notifications(session, action_type, args = {}) {
|
||||
if (!session.logged_in) throw new Error('You are not signed-in');
|
||||
|
||||
|
||||
let data;
|
||||
|
||||
switch (action_type) {
|
||||
@@ -119,11 +172,11 @@ async function notifications(session, action_type, args = {}) {
|
||||
default:
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/notification/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
|
||||
JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session })).catch((error) => error);
|
||||
const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/notification/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
|
||||
JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session })).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
if (action_type === 'modify_channel_preference') return { success: true, status_code: response.status };
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status_code: response.status,
|
||||
@@ -131,13 +184,28 @@ async function notifications(session, action_type, args = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Interacts with YouTube's livechat system.
|
||||
*
|
||||
* @param {object} session A valid Innertube session.
|
||||
* @param {string} action_type Type of action.
|
||||
* @param {object} args Action arguments.
|
||||
* @returns {object} { success: boolean, status_code: number, data: object } | { success: boolean, status_code: number, message: string }
|
||||
*/
|
||||
async function livechat(session, action_type, args = {}) {
|
||||
let data;
|
||||
switch (action_type) {
|
||||
case 'live_chat/get_live_chat':
|
||||
data = {
|
||||
context: session.context,
|
||||
continuation: args.ctoken
|
||||
};
|
||||
break;
|
||||
case 'live_chat/send_message':
|
||||
data = {
|
||||
context: session.context,
|
||||
params: Utils.generateMessageParams(args.channel_id, args.video_id),
|
||||
params: Utils.encodeMessageParams(args.channel_id, args.video_id),
|
||||
clientMessageId: `ytjs-${Uuid.v4()}`,
|
||||
richMessage: {
|
||||
textSegments: [{ text: args.text }]
|
||||
@@ -155,13 +223,20 @@ async function livechat(session, action_type, args = {}) {
|
||||
params: args.cmd_params
|
||||
};
|
||||
break;
|
||||
case 'updated_metadata':
|
||||
data = {
|
||||
context: session.context,
|
||||
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 Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
|
||||
JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session, params: args.params })).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status_code: response.status,
|
||||
@@ -169,23 +244,39 @@ async function livechat(session, action_type, args = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets detailed data for a video.
|
||||
*
|
||||
* @param {object} session A valid Innertube session.
|
||||
* @param {object} args Request arguments.
|
||||
* @returns {object} Video data.
|
||||
*/
|
||||
async function getVideoInfo(session, args = {}) {
|
||||
let response;
|
||||
|
||||
|
||||
!args.is_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));
|
||||
(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));
|
||||
if (response instanceof Error) throw new Error(`Could not get video info: ${response.message}`);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getContinuation(session, info = {}) {
|
||||
let data = { context: session.context };
|
||||
info.continuation_token && (data.continuation = info.continuation_token);
|
||||
|
||||
if (info.video_id) {
|
||||
data.videoId = info.video_id;
|
||||
/**
|
||||
* Requests continuation for previously performed actions.
|
||||
*
|
||||
* @param {object} session A valid Innertube session.
|
||||
* @param {object} args Continuation arguments.
|
||||
* @returns {object} { success: boolean, status_code: number, data: object } | { success: boolean, status_code: number, message: string }
|
||||
*/
|
||||
async function getContinuation(session, args = {}) {
|
||||
let data = { context: session.context };
|
||||
args.continuation_token && (data.continuation = args.continuation_token);
|
||||
|
||||
if (args.video_id) {
|
||||
data.videoId = args.video_id;
|
||||
data.racyCheckOk = true;
|
||||
data.contentCheckOk = false;
|
||||
data.autonavState = 'STATE_NONE';
|
||||
@@ -196,10 +287,10 @@ async function getContinuation(session, info = {}) {
|
||||
data.captionsRequested = false;
|
||||
}
|
||||
|
||||
const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/next${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
|
||||
JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session })).catch((error) => error);
|
||||
const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/next${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`,
|
||||
JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session })).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status_code: response.status,
|
||||
|
||||
105
lib/Constants.js
105
lib/Constants.js
@@ -5,6 +5,7 @@ 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'
|
||||
},
|
||||
@@ -46,6 +47,9 @@ module.exports = {
|
||||
},
|
||||
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';
|
||||
|
||||
let req_opts = {
|
||||
params: info.params || {},
|
||||
headers: {
|
||||
@@ -58,8 +62,8 @@ module.exports = {
|
||||
'x-youtube-client-name': info.desktop ? 1 : 2,
|
||||
'x-youtube-client-version': info.session.context.client.clientVersion,
|
||||
'x-youtube-chrome-connected': 'source=Chrome,mode=0,enable_account_consistency=true,supervised=false,consistency_enabled_by_default=false',
|
||||
'x-origin': info.desktop ? 'https://www.youtube.com' : 'https://m.youtube.com',
|
||||
'origin': info.desktop ? 'https://www.youtube.com' : 'https://m.youtube.com',
|
||||
'x-origin': origin,
|
||||
'origin': origin,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -91,6 +95,19 @@ module.exports = {
|
||||
videoId: id
|
||||
};
|
||||
},
|
||||
METADATA_KEYS: [
|
||||
'embed', 'view_count', 'average_rating',
|
||||
'length_seconds', 'channel_id', 'channel_url',
|
||||
'external_channel_id', 'is_live_content', 'is_family_safe',
|
||||
'is_unlisted', 'is_private', 'has_ypc_metadata',
|
||||
'category', 'owner_channel_name', 'publish_date',
|
||||
'upload_date', 'keywords', 'available_countries',
|
||||
'owner_profile_url'
|
||||
],
|
||||
BLACKLISTED_KEYS: [
|
||||
'is_owner_viewing', 'is_unplugged_corpus',
|
||||
'is_crawlable', 'allow_ratings', 'author'
|
||||
],
|
||||
BASE64_DIALECT: {
|
||||
NORMAL: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'.split(''),
|
||||
REVERSE: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'.split('')
|
||||
@@ -109,88 +126,14 @@ module.exports = {
|
||||
TRANSLATE_1: 'function(d,e){for(var f',
|
||||
TRANSLATE_2: 'function(d,e,f){var h=f'
|
||||
},
|
||||
// Helper functions, felt like Utils.js wasn't the right place for them:
|
||||
// Just a helper function, felt like Utils.js wasn't the right place for it:
|
||||
formatNTransformData: (data) => {
|
||||
return data
|
||||
.replace(/function\(d,e\)/g, '"function(d,e)').replace(/function\(d\)/g, '"function(d)')
|
||||
.replace(/function\(\)/g, '"function()').replace(/function\(d,e,f\)/g, '"function(d,e,f)')
|
||||
.replace(/\[function\(d,e,f\)/g, '["function(d,e,f)')
|
||||
.replace(/,b,/g, ',"b",').replace(/,b/g, ',"b"').replace(/b,/g, '"b",').replace(/b]/g, '"b"]')
|
||||
.replace(/\[b/g, '["b"').replace(/}]/g, '"]').replace(/},/g, '}",').replace(/""/g, '')
|
||||
.replace(/length]\)}"/g, 'length])}');
|
||||
},
|
||||
formatVideoData: (data, context, is_desktop) => {
|
||||
let video_details = {};
|
||||
let metadata = {};
|
||||
|
||||
if (is_desktop) {
|
||||
metadata.embed = data.microformat.playerMicroformatRenderer.embed;
|
||||
metadata.view_count = parseInt(data.videoDetails.viewCount);
|
||||
metadata.average_rating = data.videoDetails.averageRating;
|
||||
metadata.length_seconds = data.microformat.playerMicroformatRenderer.lengthSeconds;
|
||||
metadata.channel_id = data.videoDetails.channelId;
|
||||
metadata.channel_url = data.microformat.playerMicroformatRenderer.ownerProfileUrl;
|
||||
metadata.external_channel_id = data.microformat.playerMicroformatRenderer.externalChannelId;
|
||||
metadata.is_live_content = data.videoDetails.isLiveContent;
|
||||
metadata.is_family_safe = data.microformat.playerMicroformatRenderer.isFamilySafe;
|
||||
metadata.is_unlisted = data.microformat.playerMicroformatRenderer.isUnlisted;
|
||||
metadata.is_private = data.videoDetails.isPrivate;
|
||||
metadata.has_ypc_metadata = data.microformat.playerMicroformatRenderer.hasYpcMetadata;
|
||||
metadata.category = data.microformat.playerMicroformatRenderer.category;
|
||||
metadata.channel_name = data.microformat.playerMicroformatRenderer.ownerChannelName;
|
||||
metadata.publish_date = data.microformat.playerMicroformatRenderer.publishDate || 'N/A';
|
||||
metadata.upload_date = data.microformat.playerMicroformatRenderer.uploadDate || 'N/A';
|
||||
metadata.keywords = data.videoDetails.keywords || [];
|
||||
metadata.available_qualities = [...new Set(data.streamingData.adaptiveFormats.filter(v => v.qualityLabel).map(v => v.qualityLabel).sort((a, b) => +a.replace(/\D/gi, '') - +b.replace(/\D/gi, '')))];
|
||||
|
||||
video_details.id = data.videoDetails.videoId;
|
||||
video_details.title = data.videoDetails.title;
|
||||
video_details.description = data.videoDetails.shortDescription;
|
||||
video_details.thumbnail = data.videoDetails.thumbnail.thumbnails.slice(-1)[0];
|
||||
video_details.metadata = metadata;
|
||||
} else {
|
||||
const is_dislike_available = data[3].response.contents.singleColumnWatchNextResults.results.results.contents[1].slimVideoMetadataSectionRenderer.contents[1].slimVideoActionBarRenderer.buttons[1].slimMetadataToggleButtonRenderer.button.toggleButtonRenderer.defaultText.accessibility && true || false;
|
||||
|
||||
metadata.embed = data[2].playerResponse.microformat.playerMicroformatRenderer.embed;
|
||||
metadata.likes = parseInt(data[3].response.contents.singleColumnWatchNextResults.results.results.contents[1].slimVideoMetadataSectionRenderer.contents[1].slimVideoActionBarRenderer.buttons[0].slimMetadataToggleButtonRenderer.button.toggleButtonRenderer.defaultText.accessibility.accessibilityData.label.replace(/\D/g, ''));
|
||||
metadata.dislikes = is_dislike_available && parseInt(data[3].response.contents.singleColumnWatchNextResults.results.results.contents[1].slimVideoMetadataSectionRenderer.contents[1].slimVideoActionBarRenderer.buttons[1].slimMetadataToggleButtonRenderer.button.toggleButtonRenderer.defaultText.accessibility.accessibilityData.label.replace(/\D/g, '')) || 0;
|
||||
metadata.view_count = parseInt(data[2].playerResponse.videoDetails.viewCount);
|
||||
metadata.average_rating = data[2].playerResponse.videoDetails.averageRating;
|
||||
metadata.length_seconds = data[2].playerResponse.microformat.playerMicroformatRenderer.lengthSeconds;
|
||||
metadata.channel_id = data[2].playerResponse.videoDetails.channelId;
|
||||
metadata.channel_url = data[2].playerResponse.microformat.playerMicroformatRenderer.ownerProfileUrl;
|
||||
metadata.external_channel_id = data[2].playerResponse.microformat.playerMicroformatRenderer.externalChannelId;
|
||||
metadata.is_live_content = data[2].playerResponse.videoDetails.isLiveContent;
|
||||
metadata.is_family_safe = data[2].playerResponse.microformat.playerMicroformatRenderer.isFamilySafe;
|
||||
metadata.is_unlisted = data[2].playerResponse.microformat.playerMicroformatRenderer.isUnlisted;
|
||||
metadata.is_private = data[2].playerResponse.videoDetails.isPrivate;
|
||||
metadata.has_ypc_metadata = data[2].playerResponse.microformat.playerMicroformatRenderer.hasYpcMetadata;
|
||||
metadata.category = data[2].playerResponse.microformat.playerMicroformatRenderer.category;
|
||||
metadata.channel_name = data[2].playerResponse.microformat.playerMicroformatRenderer.ownerChannelName;
|
||||
metadata.publish_date = data[2].playerResponse.microformat.playerMicroformatRenderer.publishDate;
|
||||
metadata.upload_date = data[2].playerResponse.microformat.playerMicroformatRenderer.uploadDate;
|
||||
metadata.keywords = data[2].playerResponse.videoDetails.keywords;
|
||||
metadata.available_qualities = [...new Set(data[2].playerResponse.streamingData.adaptiveFormats.filter(v => v.qualityLabel).map(v => v.qualityLabel).sort((a, b) => +a.replace(/\D/gi, '') - +b.replace(/\D/gi, '')))];
|
||||
|
||||
video_details.id = data[2].playerResponse.videoDetails.videoId;
|
||||
video_details.title = data[2].playerResponse.videoDetails.title;
|
||||
video_details.description = data[2].playerResponse.videoDetails.shortDescription;
|
||||
video_details.thumbnail = data[2].playerResponse.videoDetails.thumbnail.thumbnails.slice(-1)[0];
|
||||
|
||||
// Placeholders for functions
|
||||
video_details.like = () => {};
|
||||
video_details.dislike = () => {};
|
||||
video_details.removeLike = () => {};
|
||||
video_details.subscribe = () => {};
|
||||
video_details.unsubscribe = () => {};
|
||||
video_details.comment = () => {};
|
||||
video_details.getComments = () => {};
|
||||
video_details.setNotificationPref = () => {};
|
||||
video_details.getLivechat = () => {};
|
||||
|
||||
// Additional metadata
|
||||
video_details.metadata = metadata;
|
||||
}
|
||||
return video_details;
|
||||
.replace(/\[function\(d,e,f\)/g, '["function(d,e,f)').replace(/,b,/g, ',"b",')
|
||||
.replace(/,b/g, ',"b"').replace(/b,/g, '"b",').replace(/b]/g, '"b"]')
|
||||
.replace(/\[b/g, '["b"').replace(/}]/g, '"]').replace(/},/g, '}",')
|
||||
.replace(/""/g, '').replace(/length]\)}"/g, 'length])}');
|
||||
}
|
||||
};
|
||||
182
lib/Innertube.js
182
lib/Innertube.js
@@ -5,6 +5,7 @@ const Stream = require('stream');
|
||||
const OAuth = require('./OAuth');
|
||||
const Utils = require('./Utils');
|
||||
const Player = require('./Player');
|
||||
const Parser = require('./Parser');
|
||||
const NToken = require('./NToken');
|
||||
const Actions = require('./Actions');
|
||||
const Livechat = require('./Livechat');
|
||||
@@ -18,26 +19,28 @@ class Innertube {
|
||||
constructor(cookie) {
|
||||
this.cookie = cookie || '';
|
||||
this.retry_count = 0;
|
||||
return this.init();
|
||||
return this.#init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
async #init() {
|
||||
const response = await Axios.get(Constants.URLS.YT_BASE_URL, Constants.DEFAULT_HEADERS(this)).catch((error) => error);
|
||||
if (response instanceof Error) throw new Error(`Could not extract Innertube data: ${response.message}`);
|
||||
if (response instanceof Error) throw new Error(`Could not retrieve Innertube session: ${response.message}`);
|
||||
|
||||
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;
|
||||
const data = JSON.parse(`{${Utils.getStringBetweenStrings(response.data, 'ytcfg.set({', '});')}}`);
|
||||
if (data.INNERTUBE_CONTEXT) {
|
||||
this.context = data.INNERTUBE_CONTEXT;
|
||||
this.key = data.INNERTUBE_API_KEY;
|
||||
this.id_token = data.ID_TOKEN;
|
||||
this.session_token = data.XSRF_TOKEN;
|
||||
this.player_url = data.PLAYER_JS_URL;
|
||||
this.logged_in = data.LOGGED_IN;
|
||||
this.sts = data.STS;
|
||||
this.context.client.hl = 'en';
|
||||
this.context.client.gl = 'US';
|
||||
|
||||
this.ev = new EventEmitter();
|
||||
|
||||
this.player = new Player(this);
|
||||
await this.player.init();
|
||||
|
||||
@@ -45,36 +48,32 @@ class Innertube {
|
||||
this.auth_apisid = Utils.getStringBetweenStrings(this.cookie, 'PAPISID=', ';');
|
||||
this.auth_apisid = Utils.generateSidAuth(this.auth_apisid);
|
||||
}
|
||||
|
||||
this.ev = new EventEmitter();
|
||||
} else {
|
||||
this.retry_count += 1;
|
||||
if (this.retry_count >= 10) throw new Error('Could not retrieve Innertube data');
|
||||
return this.init();
|
||||
throw new Error('Could not retrieve Innertube session due to unknown reasons');
|
||||
}
|
||||
} catch (err) {
|
||||
this.retry_count += 1;
|
||||
if (this.retry_count >= 10) throw new Error('Could not retrieve Innertube data');
|
||||
return this.init();
|
||||
if (this.retry_count >= 10) throw new Error(`Could not retrieve Innertube session: ${err.message}`);
|
||||
return this.#init();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs-in to a google account.
|
||||
*
|
||||
* @param {object} auth_info { refresh_token: string, access_token: string, expires: string }
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
signIn(auth_info = {}) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const oauth = new OAuth(auth_info);
|
||||
if (auth_info.access_token) {
|
||||
const is_valid = await oauth.isTokenValid(auth_info.expires);
|
||||
|
||||
if (!is_valid) {
|
||||
const new_tokens = await oauth.refreshAccessToken(auth_info.refresh_token);
|
||||
if (!oauth.isTokenValid()) {
|
||||
const new_tokens = await oauth.refreshAccessToken();
|
||||
auth_info.refresh_token = new_tokens.credentials.refresh_token;
|
||||
auth_info.access_token = new_tokens.credentials.access_token;
|
||||
|
||||
this.ev.emit('update-credentials', {
|
||||
credentials: new_tokens.credentials,
|
||||
status: new_tokens.status
|
||||
});
|
||||
this.ev.emit('update-credentials', { credentials: new_tokens.credentials, status: new_tokens.status });
|
||||
}
|
||||
|
||||
this.access_token = auth_info.access_token;
|
||||
@@ -85,15 +84,10 @@ class Innertube {
|
||||
} else {
|
||||
oauth.on('auth', (data) => {
|
||||
if (data.status === 'SUCCESS') {
|
||||
this.ev.emit('auth', { credentials: data.credentials, status: data.status });
|
||||
this.access_token = data.credentials.access_token;
|
||||
this.refresh_token = data.credentials.refresh_token;
|
||||
this.logged_in = true;
|
||||
|
||||
this.ev.emit('auth', {
|
||||
credentials: data.credentials,
|
||||
status: data.status
|
||||
});
|
||||
|
||||
resolve();
|
||||
} else {
|
||||
this.ev.emit('auth', data);
|
||||
@@ -103,74 +97,63 @@ class Innertube {
|
||||
});
|
||||
}
|
||||
|
||||
async search(query, options = { period: 'any', order: 'relevance', duration: 'any' }) {
|
||||
const response = await Actions.search(this, { query, options });
|
||||
/**
|
||||
* Searches on YouTube.
|
||||
*
|
||||
* @param {string} query Search query.
|
||||
* @param {object} options { client: YOUTUBE | YTMUSIC, period: any | hour | day | week | month | year , order: relevance | rating | age | views, duration: any | short | long }
|
||||
* @returns {Promise<object>} Search results.
|
||||
*/
|
||||
async search(query, options = { client: 'YOUTUBE', period: 'any', order: 'relevance', duration: 'any' }) {
|
||||
const response = await Actions.search(this, options.client, { query, options });
|
||||
if (!response.success) throw new Error(`Could not search on YouTube: ${response.message}`);
|
||||
|
||||
const content = response.data.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents[0].itemSectionRenderer.contents;
|
||||
const search = {};
|
||||
const refined_data = new Parser(response.data, {
|
||||
client: options.client,
|
||||
data_type: 'SEARCH',
|
||||
query
|
||||
}).parse();
|
||||
|
||||
search.search_metadata = {};
|
||||
search.search_metadata.query = content[0].showingResultsForRenderer ? content[0].showingResultsForRenderer.originalQuery.simpleText : query;
|
||||
search.search_metadata.corrected_query = content[0].showingResultsForRenderer ? content[0].showingResultsForRenderer.correctedQueryEndpoint.searchEndpoint.query : query;
|
||||
search.search_metadata.estimated_results = parseInt(response.data.estimatedResults);
|
||||
search.videos = content.map((data) => {
|
||||
if (!data.videoRenderer) return;
|
||||
const 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',
|
||||
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: {
|
||||
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'
|
||||
},
|
||||
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);
|
||||
return search;
|
||||
return refined_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets details for a video.
|
||||
*
|
||||
* @param {string} id The id of the video.
|
||||
*/
|
||||
async getDetails(id) {
|
||||
if (!id) throw new Error('You must provide a video id');
|
||||
|
||||
const data = await Actions.getVideoInfo(this, { id, is_desktop: false });
|
||||
const video_data = Constants.formatVideoData(data, this, false);
|
||||
const refined_data = new Parser(data, { client: 'YOUTUBE', data_type: 'VIDEO_INFO', desktop_v: false }).parse();
|
||||
|
||||
if (video_data.metadata.is_live_content) {
|
||||
if (refined_data.metadata.is_live_content) {
|
||||
const data_continuation = await Actions.getContinuation(this, { video_id: id });
|
||||
if (!data_continuation.data.contents.twoColumnWatchNextResults.conversationBar) return;
|
||||
video_data.getLivechat = () => new Livechat(this, data_continuation.data.contents.twoColumnWatchNextResults.conversationBar.liveChatRenderer.continuations[0].reloadContinuationData.continuation, video_data.metadata.channel_id, id);
|
||||
refined_data.getLivechat = () => new Livechat(this, data_continuation.data.contents.twoColumnWatchNextResults.conversationBar.liveChatRenderer.continuations[0].reloadContinuationData.continuation, refined_data.metadata.channel_id, id);
|
||||
} else {
|
||||
video_data.getLivechat = () => {};
|
||||
refined_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 });
|
||||
video_data.subscribe = () => Actions.engage(this, 'subscription/subscribe', { video_id: id, channel_id: video_data.metadata.channel_id });
|
||||
video_data.unsubscribe = () => Actions.engage(this, 'subscription/unsubscribe', { video_id: id, channel_id: video_data.metadata.channel_id });
|
||||
video_data.comment = text => Actions.engage(this, 'comment/create_comment', { video_id: id, text });
|
||||
video_data.getComments = () => this.getComments(id);
|
||||
video_data.setNotificationPref = pref => Actions.notifications(this, 'modify_channel_preference', { channel_id: video_data.metadata.channel_id, pref: pref || 'NONE' });
|
||||
refined_data.like = () => Actions.engage(this, 'like/like', { video_id: id });
|
||||
refined_data.dislike = () => Actions.engage(this, 'like/dislike', { video_id: id });
|
||||
refined_data.removeLike = () => Actions.engage(this, 'like/removelike', { video_id: id });
|
||||
refined_data.subscribe = () => Actions.engage(this, 'subscription/subscribe', { video_id: id, channel_id: refined_data.metadata.channel_id });
|
||||
refined_data.unsubscribe = () => Actions.engage(this, 'subscription/unsubscribe', { video_id: id, channel_id: refined_data.metadata.channel_id });
|
||||
refined_data.comment = text => Actions.engage(this, 'comment/create_comment', { video_id: id, text });
|
||||
refined_data.getComments = () => this.getComments(id);
|
||||
refined_data.setNotificationPref = pref => Actions.notifications(this, 'modify_channel_preference', { channel_id: refined_data.metadata.channel_id, pref: pref || 'NONE' });
|
||||
|
||||
return video_data;
|
||||
return refined_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the comments section of a video.
|
||||
*
|
||||
* @param {string} video_id The id of the video.
|
||||
* @param {string} token Continuation token (optional).
|
||||
*/
|
||||
async getComments(video_id, token) {
|
||||
let comment_section_token;
|
||||
|
||||
@@ -220,6 +203,10 @@ class Innertube {
|
||||
return comments_section;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns your subscription feed.
|
||||
* @returns {Promise<object>} subs feed.
|
||||
*/
|
||||
async getSubscriptionsFeed() {
|
||||
const response = await Actions.browse(this, 'subscriptions_feed');
|
||||
if (!response.success) throw new Error('Could not fetch subscriptions feed');
|
||||
@@ -232,9 +219,7 @@ class Innertube {
|
||||
|
||||
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 = {
|
||||
@@ -257,6 +242,10 @@ class Innertube {
|
||||
return subscriptions_feed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns your notifications.
|
||||
* @returns {Promise<object>} notifications.
|
||||
*/
|
||||
async getNotifications() {
|
||||
const response = await Actions.notifications(this, 'get_notification_menu');
|
||||
if (!response.success) throw new Error('Could not fetch notifications');
|
||||
@@ -279,12 +268,22 @@ class Innertube {
|
||||
}).filter((notification_block) => notification_block);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of notifications you haven't seen.
|
||||
* @returns {Promise<number>} unseen notifications count.
|
||||
*/
|
||||
async getUnseenNotificationsCount() {
|
||||
const response = await Actions.notifications(this, 'get_unseen_count');
|
||||
if (!response.success) throw new Error('Could not fetch unseen notifications count');
|
||||
if (!response.success) throw new Error('Could not get unseen notifications count');
|
||||
return response.data.unseenCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a video from YouTube.
|
||||
*
|
||||
* @param {string} id The id of the video.
|
||||
* @param {object} options Download options: { quality?: string, type?: string, format?: string }
|
||||
*/
|
||||
download(id, options = {}) {
|
||||
if (!id) throw new Error('Missing video id');
|
||||
|
||||
@@ -326,8 +325,6 @@ class Innertube {
|
||||
formats.hls_manifest_url = video_data.streamingData.hlsManifestUrl || undefined;
|
||||
formats.dash_manifest_url = video_data.streamingData.dashManifestUrl || undefined;
|
||||
|
||||
const video_details = Constants.formatVideoData(video_data, this, true);
|
||||
|
||||
let url;
|
||||
let bitrates;
|
||||
let filtered_streams;
|
||||
@@ -363,7 +360,8 @@ class Innertube {
|
||||
if (!selected_format) {
|
||||
return stream.emit('error', { message: 'Could not find any suitable format.', type: 'FORMAT_UNAVAILABLE' });
|
||||
} else {
|
||||
stream.emit('info', { video_details, selected_format, formats });
|
||||
const refined_data = new Parser(video_data, { client: 'YOUTUBE', data_type: 'VIDEO_INFO', desktop_v: true }).parse();
|
||||
stream.emit('info', { video_details: refined_data, selected_format, formats });
|
||||
}
|
||||
|
||||
if (options.type == 'videoandaudio' && !options.range) {
|
||||
@@ -390,8 +388,8 @@ class Innertube {
|
||||
});
|
||||
|
||||
response.data.on('error', (err) => {
|
||||
cancelled && stream.emit('error', { message: 'The download was cancelled.', type: 'DOWNLOAD_CANCELLED' }) ||
|
||||
stream.emit('error', { message: err.message, type: 'DOWNLOAD_ABORTED' });
|
||||
cancelled && stream.emit('error', { message: 'The download was cancelled.', type: 'DOWNLOAD_CANCELLED' })
|
||||
|| stream.emit('error', { message: err.message, type: 'DOWNLOAD_ABORTED' });
|
||||
});
|
||||
|
||||
response.data.pipe(stream, { end: true });
|
||||
|
||||
184
lib/Parser.js
Normal file
184
lib/Parser.js
Normal file
@@ -0,0 +1,184 @@
|
||||
'use strict';
|
||||
|
||||
const Utils = require('./Utils')
|
||||
const Constants = require('./Constants');
|
||||
const TimeToSeconds = require('time-to-seconds');
|
||||
|
||||
/**
|
||||
* Takes raw data from the Innertube API and refines it.
|
||||
*/
|
||||
class Parser {
|
||||
constructor(data, args = {}) {
|
||||
this.data = data;
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
parse() {
|
||||
return this.args.client === 'YOUTUBE' ? ({
|
||||
SEARCH: () => this.#parseVideoSearch(),
|
||||
VIDEO_INFO: () => this.#parseVideoInfo()
|
||||
})[this.args.data_type]() : ({
|
||||
SEARCH: () => this.#parseMusicSearch(),
|
||||
SONG_INFO: () => { }
|
||||
})[this.args.data_type]();
|
||||
}
|
||||
|
||||
#parseVideoSearch() {
|
||||
const response = {};
|
||||
|
||||
const contents = this.data.contents.twoColumnSearchResultsRenderer
|
||||
.primaryContents.sectionListRenderer.contents[0].itemSectionRenderer
|
||||
.contents;
|
||||
|
||||
const continuation_token = this.data.contents.twoColumnSearchResultsRenderer
|
||||
.primaryContents.sectionListRenderer.contents[1].continuationItemRenderer
|
||||
.continuationEndpoint.continuationCommand.token;
|
||||
|
||||
response.search_metadata = {};
|
||||
response.search_metadata.query = contents[0].showingResultsForRenderer && contents[0].showingResultsForRenderer.originalQuery.simpleText || this.args.query;
|
||||
response.search_metadata.corrected_query = contents[0].showingResultsForRenderer && contents[0].showingResultsForRenderer.correctedQueryEndpoint.searchEndpoint.query || this.args.query;
|
||||
response.search_metadata.estimated_results = parseInt(this.data.estimatedResults);
|
||||
|
||||
response.videos = contents.map((data) => {
|
||||
if (!data.videoRenderer) return;
|
||||
const 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',
|
||||
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: {
|
||||
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'
|
||||
},
|
||||
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 '
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
#parseMusicSearch() {
|
||||
const tabs = this.data.contents.tabbedSearchResultsRenderer.tabs;
|
||||
const contents = tabs[0].tabRenderer.content.sectionListRenderer.contents;
|
||||
|
||||
const songs_ms = contents.find((content) => content.musicShelfRenderer.title.runs[0].text == 'Songs');
|
||||
const songs = songs_ms.musicShelfRenderer.contents.map((item) => {
|
||||
const list_item = item.musicResponsiveListItemRenderer;
|
||||
return {
|
||||
id: list_item.playlistItemData.videoId,
|
||||
title: list_item.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text,
|
||||
artist: list_item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[2].text,
|
||||
album: list_item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[4].text,
|
||||
duration: list_item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[6].text,
|
||||
thumbnail: list_item.thumbnail.musicThumbnailRenderer.thumbnail
|
||||
};
|
||||
});
|
||||
|
||||
const videos_ms = contents.find((content) => content.musicShelfRenderer.title.runs[0].text == 'Videos');
|
||||
const videos = videos_ms.musicShelfRenderer.contents.map((item) => {
|
||||
const list_item = item.musicResponsiveListItemRenderer;
|
||||
return {
|
||||
id: list_item.playlistItemData.videoId,
|
||||
title: list_item.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text,
|
||||
author: list_item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[2].text,
|
||||
views: list_item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[4].text,
|
||||
duration: list_item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[6].text,
|
||||
thumbnail: list_item.thumbnail.musicThumbnailRenderer.thumbnail
|
||||
};
|
||||
});
|
||||
|
||||
const albums_ms = contents.find((content) => content.musicShelfRenderer.title.runs[0].text == 'Albums');
|
||||
const albums = albums_ms.musicShelfRenderer.contents.map((item) => {
|
||||
const list_item = item.musicResponsiveListItemRenderer;
|
||||
return {
|
||||
title: list_item.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text,
|
||||
author: list_item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[2].text,
|
||||
year: list_item.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs.find((run) => /^[12][0-9]{3}$/.test(run.text)).text,
|
||||
thumbnail: list_item.thumbnail.musicThumbnailRenderer.thumbnail
|
||||
};
|
||||
});
|
||||
|
||||
return { songs, videos, albums };
|
||||
}
|
||||
|
||||
#parseVideoInfo() {
|
||||
const desktop_v = this.args.desktop_v;
|
||||
|
||||
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;
|
||||
|
||||
const response = { metadata: {} };
|
||||
const mf_raw_data = Object.entries(microformat);
|
||||
const dt_raw_data = Object.entries(details);
|
||||
|
||||
mf_raw_data.forEach((entry) => {
|
||||
const key = Utils.camelToSnake(entry[0]);
|
||||
if (Constants.METADATA_KEYS.includes(key)) {
|
||||
key == 'view_count' && (response.metadata[key] = parseInt(entry[1])) ||
|
||||
key == 'owner_profile_url' && (response.metadata.channel_url = entry[1]) ||
|
||||
key == 'owner_channel_name' && (response.metadata.channel_name = entry[1]) ||
|
||||
(response.metadata[key] = entry[1]);
|
||||
} else {
|
||||
response[key] = entry[1];
|
||||
}
|
||||
});
|
||||
|
||||
dt_raw_data.forEach((entry) => {
|
||||
const key = Utils.camelToSnake(entry[0]);
|
||||
if (Constants.BLACKLISTED_KEYS.includes(key)) return;
|
||||
if (Constants.METADATA_KEYS.includes(key)) {
|
||||
key == 'view_count' && (response.metadata[key] = parseInt(entry[1])) ||
|
||||
(response.metadata[key] = entry[1]);
|
||||
} else {
|
||||
response[key] = entry[1];
|
||||
key == 'short_description' && (response.description = entry[1]);
|
||||
key == 'thumbnail' && (response.thumbnail = entry[1].thumbnails.slice(-1)[0]);
|
||||
key == 'video_id' && (response.id = 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;
|
||||
}
|
||||
|
||||
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, '')))];
|
||||
|
||||
delete response.video_id;
|
||||
delete response.short_description;
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Parser;
|
||||
Reference in New Issue
Block a user