youtubei.js
Advanced tools
Comparing version 1.2.8 to 1.2.9
@@ -8,5 +8,13 @@ 'use strict'; | ||
/** | ||
* 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; | ||
@@ -35,3 +43,3 @@ switch (engagement_type) { | ||
commentText: args.text, | ||
createCommentParams: Utils.generateCommentParams(args.video_id) | ||
createCommentParams: Utils.encodeCommentParams(args.video_id) | ||
}; | ||
@@ -42,7 +50,7 @@ break; | ||
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 { | ||
@@ -54,3 +62,10 @@ success: true, | ||
async function browse(session, action_type) { | ||
/** | ||
* 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, args) { | ||
if (!session.logged_in) throw new Error('You are not signed-in'); | ||
@@ -60,2 +75,8 @@ | ||
switch (action_type) { // TODO: Handle more actions | ||
case 'home_feed': | ||
data = { | ||
context: session.context, | ||
browseId: 'FEwhat_to_watch' | ||
}; | ||
break; | ||
case 'subscriptions_feed': | ||
@@ -70,7 +91,8 @@ data = { | ||
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 client_domain = args.ytmusic && Constants.URLS.YT_MUSIC_URL || Constants.URLS.YT_BASE_URL; | ||
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 { | ||
@@ -83,13 +105,42 @@ success: true, | ||
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 { | ||
@@ -102,5 +153,14 @@ success: true, | ||
/** | ||
* 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; | ||
@@ -130,7 +190,7 @@ | ||
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 { | ||
@@ -143,9 +203,24 @@ success: true, | ||
/** | ||
* 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()}`, | ||
@@ -168,22 +243,33 @@ richMessage: { | ||
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); | ||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message }; | ||
return { | ||
success: true, | ||
status_code: response.status, | ||
data: response.data | ||
}; | ||
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, message: response.message }; | ||
return { success: true, data: response.data }; | ||
} | ||
/** | ||
* 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}`); | ||
@@ -194,22 +280,43 @@ | ||
async function getContinuation(session, info = {}) { | ||
/** | ||
* 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 }; | ||
info.continuation_token && (data.continuation = info.continuation_token); | ||
args.continuation_token && (data.continuation = args.continuation_token); | ||
if (info.video_id) { | ||
data.videoId = info.video_id; | ||
data.racyCheckOk = true; | ||
data.contentCheckOk = false; | ||
data.autonavState = 'STATE_NONE'; | ||
data.playbackContext = { | ||
vis: 0, | ||
lactMilliseconds: '-1' | ||
}; | ||
data.captionsRequested = false; | ||
if (args.video_id) { | ||
data.videoId = args.video_id; | ||
if (args.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; | ||
data.isAudioOnly = true; | ||
data.tunerSettingValue = 'AUTOMIX_SETTING_NORMAL'; | ||
} else { | ||
data.racyCheckOk = true; | ||
data.contentCheckOk = false; | ||
data.autonavState = 'STATE_NONE'; | ||
data.playbackContext = { | ||
vis: 0, | ||
lactMilliseconds: '-1' | ||
} | ||
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 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 }; | ||
return { | ||
@@ -222,2 +329,2 @@ success: true, | ||
module.exports = { engage, browse, search, notifications, livechat, getContinuation }; | ||
module.exports = { engage, browse, search, notifications, livechat, getVideoInfo, getContinuation }; |
@@ -8,2 +8,3 @@ 'use strict'; | ||
YT_BASE_URL: 'https://www.youtube.com', | ||
YT_MUSIC_URL: 'https://music.youtube.com', | ||
YT_MOBILE_URL: 'https://m.youtube.com', | ||
@@ -50,2 +51,5 @@ YT_WATCH_PAGE: 'https://m.youtube.com/watch' | ||
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 = { | ||
@@ -63,4 +67,4 @@ params: info.params || {}, | ||
'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, | ||
} | ||
@@ -97,2 +101,15 @@ }; | ||
}, | ||
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: { | ||
@@ -102,3 +119,3 @@ NORMAL: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'.split(''), | ||
}, | ||
FUNCS_REGEX: /d\.push\(e\)|d\.reverse\(\)|d\[0\]\)\[0\]\)|f=d\[0];d\[0\]|d\.length;d\.splice\(e,1\)|function\(\){for\(var|function\(d,e,f\){var h=f|function\(d\){for\(var|reverse\(\)\.forEach|unshift\(d\.pop\(\)\)|function\(d,e\){for\(var f/, | ||
FUNCS_REGEX: /d\.push\(e\)|d\.reverse\(\)|d\[0\]\)\[0\]\)|f=d\[0];d\[0\]|d\.length;d\.splice\(e,1\)|function\(\){for\(var|function\(d,e,f\){var k=f|function\(d\){for\(var|reverse\(\)\.forEach|unshift\(d\.pop\(\)\)|function\(d,e\){for\(var f/, | ||
FUNCS: { | ||
@@ -115,5 +132,5 @@ PUSH: 'd.push(e)', | ||
TRANSLATE_1: 'function(d,e){for(var f', | ||
TRANSLATE_2: 'function(d,e,f){var h=f' | ||
TRANSLATE_2: 'function(d,e,f){var k=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) => { | ||
@@ -123,81 +140,7 @@ return data | ||
.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])}'); | ||
} | ||
}; |
@@ -8,2 +8,3 @@ 'use strict'; | ||
const Player = require('./Player'); | ||
const Parser = require('./Parser'); | ||
const NToken = require('./NToken'); | ||
@@ -15,3 +16,2 @@ const Actions = require('./Actions'); | ||
const EventEmitter = require('events'); | ||
const TimeToSeconds = require('time-to-seconds'); | ||
const CancelToken = Axios.CancelToken; | ||
@@ -23,22 +23,24 @@ | ||
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); | ||
@@ -51,13 +53,9 @@ await this.player.init(); | ||
} | ||
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(); | ||
} | ||
@@ -67,2 +65,8 @@ 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 = {}) { | ||
@@ -72,19 +76,13 @@ return new Promise(async (resolve, reject) => { | ||
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); | ||
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 | ||
}); | ||
if (!oauth.isTokenValid()) { | ||
const tokens = await oauth.refreshAccessToken(); | ||
auth_info.refresh_token = tokens.credentials.refresh_token; | ||
auth_info.access_token = tokens.credentials.access_token; | ||
this.ev.emit('update-credentials', { credentials: tokens.credentials, status: tokens.status }); | ||
} | ||
this.access_token = auth_info.access_token; | ||
this.refresh_token = auth_info.refresh_token; | ||
this.logged_in = true; | ||
resolve(); | ||
@@ -94,11 +92,6 @@ } else { | ||
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(); | ||
@@ -113,44 +106,27 @@ } else { | ||
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 = {}; | ||
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; | ||
const refined_data = new Parser(this, response.data, { | ||
client: options.client, | ||
data_type: 'SEARCH', | ||
query | ||
}).parse(); | ||
return refined_data; | ||
} | ||
/** | ||
* Gets details for a video. | ||
* | ||
* @param {string} id The id of the video. | ||
*/ | ||
async getDetails(id) { | ||
@@ -160,24 +136,33 @@ 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(this, 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); | ||
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, id); | ||
} else { | ||
refined_data.getLivechat = () => { }; | ||
} | ||
} 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) { | ||
@@ -232,5 +217,39 @@ let comment_section_token; | ||
/** | ||
* Returns YouTube's home feed. | ||
* @returns {Promise<object>} home feed. | ||
*/ | ||
async getHomeFeed() { | ||
const response = await Actions.browse(this, 'home_feed'); | ||
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; | ||
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', | ||
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] || [], | ||
published: content.videoRenderer.publishedTimeText && content.videoRenderer.publishedTimeText.simpleText || 'N/A', | ||
badges: content.videoRenderer.badges && content.videoRenderer.badges.map((badge) => badge.metadataBadgeRenderer.label) || 'N/A', | ||
owner_badges: content.videoRenderer.ownerBadges && content.videoRenderer.ownerBadges.map((badge) => badge.metadataBadgeRenderer.tooltip) || 'N/A' | ||
} | ||
} | ||
}).filter((video) => video); | ||
} | ||
/** | ||
* 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'); | ||
if (!response.success) throw new Error('Could not get subscriptions feed'); | ||
@@ -251,8 +270,8 @@ const contents = response.data.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents; | ||
const content = { | ||
id: item.gridVideoRenderer.videoId, | ||
title: item.gridVideoRenderer.title.runs.map((run) => run.text).join(' '), | ||
id: item.gridVideoRenderer.videoId, | ||
channel: item.gridVideoRenderer.shortBylineText && item.gridVideoRenderer.shortBylineText.runs[0].text || 'N/A', | ||
metadata: { | ||
view_count: item.gridVideoRenderer.viewCountText && item.gridVideoRenderer.viewCountText.simpleText || 'N/A', | ||
thumbnail: item.gridVideoRenderer.thumbnail && item.gridVideoRenderer.thumbnail.thumbnails || [], | ||
thumbnail: item.gridVideoRenderer.thumbnail && item.gridVideoRenderer.thumbnail.thumbnails.slice(-1)[0] || [], | ||
published: item.gridVideoRenderer.publishedTimeText && item.gridVideoRenderer.publishedTimeText.simpleText || 'N/A', | ||
@@ -271,2 +290,6 @@ badges: item.gridVideoRenderer.badges && item.gridVideoRenderer.badges.map((badge) => badge.metadataBadgeRenderer.label) || 'N/A', | ||
/** | ||
* Returns your notifications. | ||
* @returns {Promise<object>} notifications. | ||
*/ | ||
async getNotifications() { | ||
@@ -294,8 +317,18 @@ const response = await Actions.notifications(this, 'get_notification_menu'); | ||
/** | ||
* 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 = {}) { | ||
@@ -342,4 +375,2 @@ if (!id) throw new Error('Missing video id'); | ||
const video_details = Constants.formatVideoData(video_data, this, true); | ||
let url; | ||
@@ -380,3 +411,4 @@ let bitrates; | ||
} else { | ||
stream.emit('info', { video_details, selected_format, formats }); | ||
const refined_data = new Parser(this, video_data, { client: 'YOUTUBE', data_type: 'VIDEO_INFO', desktop_v: true }).parse(); | ||
stream.emit('info', { video_details: refined_data, selected_format, formats }); | ||
} | ||
@@ -399,3 +431,3 @@ | ||
let downloaded_size = 0; | ||
response.data.on('data', (chunk) => { | ||
@@ -416,3 +448,3 @@ downloaded_size += chunk.length; | ||
const chunk_size = 1048576 * 10; // 10MB | ||
let chunk_start = (options.range && options.range.start || 0); | ||
@@ -419,0 +451,0 @@ let chunk_end = (options.range && options.range.end || chunk_size); |
@@ -22,41 +22,19 @@ 'use strict'; | ||
this.poll(); | ||
this.#poll(); | ||
} | ||
enqueueActionGroup(group) { | ||
group.forEach((action) => { | ||
if (!action.addChatItemAction) return; //TODO: handle different action types | ||
const message_content = action.addChatItemAction.item.liveChatTextMessageRenderer; | ||
if (!message_content) return; | ||
const message = { | ||
text: message_content.message.runs.map((item) => item.text).join(' '), | ||
author: { | ||
name: message_content.authorName && message_content.authorName.simpleText || 'N/', | ||
channel_id: message_content.authorExternalChannelId, | ||
profile_picture: message_content.authorPhoto.thumbnails | ||
}, | ||
timestamp: message_content.timestampUsec, | ||
id: message_content.id | ||
}; | ||
this.message_queue.push(message); | ||
}); | ||
} | ||
async poll() { | ||
async #poll() { | ||
if (!this.running) return; | ||
let data; | ||
const livechat = await Actions.livechat(this.session, 'live_chat/get_live_chat', { ctoken: this.ctoken }); | ||
if (!livechat.success) { | ||
this.emit('error', { message: `Failed polling livechat: ${livechat.message}. Retrying...` }); | ||
return await this.#poll(); | ||
} | ||
data = { context: this.session.context, continuation: this.ctoken }; | ||
const livechat = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/live_chat/get_live_chat${this.session.logged_in && this.session.cookie.length < 1 ? '' : `?key=${this.session.key}`}`, JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session: this.session, data, desktop: true })); | ||
if (livechat instanceof Error) throw new Error(`Error polling livechat: ${livechat.message}`); | ||
const continuation_contents = livechat.data.continuationContents; | ||
const action_group = continuation_contents.liveChatContinuation.actions; | ||
this.enqueueActionGroup(action_group); | ||
this.#enqueueActionGroup(action_group); | ||
// Why don't we just emit the message directly? Well, enqueueing the messages is necessary so they are not emitted in a “messy” way, funny enough that's exactly how YouTube does it in its live chat js script. | ||
this.message_queue.forEach((message, index) => { | ||
this.message_queue.forEach((message) => { | ||
if (this.id_cache.includes(message.id)) return; | ||
@@ -69,7 +47,10 @@ setTimeout(() => this.emit('chat-update', message), message.timestamp / 1000 - new Date().getTime()); | ||
data = { context: this.session.context, videoId: this.video_id }; | ||
const data = { video_id: this.video_id }; | ||
if (this.metadata_ctoken) data.continuation = this.metadata_ctoken; | ||
const updated_metadata = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/updated_metadata${this.session.logged_in && this.session.cookie.length < 1 ? '' : `?key=${this.session.key}`}`, JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session: this.session, data, desktop: true })); | ||
if (updated_metadata instanceof Error) throw new Error(`Error polling updated metadata: ${updated_metadata.message}`); | ||
const updated_metadata = await Actions.livechat(this.session, 'updated_metadata', data); | ||
if (!updated_metadata.success) { | ||
this.emit('error', { message: `Failed polling livechat metadata: ${livechat.message}.` }); | ||
} | ||
this.metadata_ctoken = updated_metadata.data.continuation.timedContinuationData.continuation; | ||
@@ -80,3 +61,2 @@ | ||
likes: metadata[1].updateToggleButtonTextAction.defaultText.simpleText, | ||
dislikes: metadata[2].updateToggleButtonTextAction.defaultText.simpleText, | ||
view_count: { | ||
@@ -88,5 +68,26 @@ simple_text: metadata[0].updateViewershipAction.viewCount.videoViewCountRenderer.viewCount.simpleText, | ||
this.livechat_poller = setTimeout(async () => await this.poll(), this.poll_intervals_ms); | ||
this.livechat_poller = setTimeout(async () => await this.#poll(), this.poll_intervals_ms); | ||
} | ||
#enqueueActionGroup(group) { | ||
group.forEach((action) => { | ||
if (!action.addChatItemAction) return; //TODO: handle different action types | ||
const message_content = action.addChatItemAction.item.liveChatTextMessageRenderer; | ||
if (!message_content) return; | ||
const message = { | ||
text: message_content.message.runs.map((item) => item.text).join(' '), | ||
author: { | ||
name: message_content.authorName && message_content.authorName.simpleText || 'N/', | ||
channel_id: message_content.authorExternalChannelId, | ||
profile_picture: message_content.authorPhoto.thumbnails | ||
}, | ||
timestamp: message_content.timestampUsec, | ||
id: message_content.id | ||
}; | ||
this.message_queue.push(message); | ||
}); | ||
} | ||
async sendMessage(text) { | ||
@@ -111,3 +112,3 @@ const message = await Actions.livechat(this.session, 'live_chat/send_message', { text, channel_id: this.channel_id, video_id: this.video_id }); | ||
status_code: message.status_code, | ||
deleteMessage, | ||
deleteMessage: deleteMessage, | ||
message_data: { | ||
@@ -114,0 +115,0 @@ text: message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.message.runs.map((item) => item.text).join(' '), |
@@ -13,2 +13,8 @@ 'use strict'; | ||
/** | ||
* Solves throttling challange by transforming the n token. | ||
* | ||
* @param {string} n token. | ||
* @returns {string} transformed token. | ||
*/ | ||
transform(n) { | ||
@@ -18,19 +24,19 @@ let n_token = n.split(''); | ||
try { | ||
let transformations = this.getTransformationData(); | ||
let transformations = this.#getTransformationData(); | ||
transformations = transformations.map((el) => { | ||
if (el != null && typeof el != 'number') { | ||
const is_reverse_base64 = el.includes('case 65:'); | ||
(({ // Identifies the transformation functions and emulates them accordingly. | ||
[Constants.FUNCS.PUSH]: () => el = (arr, i) => this.push(arr, i), | ||
[Constants.FUNCS.SPLICE]: () => el = (arr, i) => this.splice(arr, i), | ||
[Constants.FUNCS.SWAP0_1]: () => el = (arr, i) => this.swap0(arr, i), | ||
[Constants.FUNCS.SWAP0_2]: () => el = (arr, i) => this.swap0(arr, i), | ||
[Constants.FUNCS.ROTATE_1]: () => el = (arr, i) => this.rotate(arr, i), | ||
[Constants.FUNCS.ROTATE_2]: () => el = (arr, i) => this.rotate(arr, i), | ||
[Constants.FUNCS.REVERSE_1]: () => el = (arr) => this.reverse(arr), | ||
[Constants.FUNCS.REVERSE_2]: () => el = (arr) => this.reverse(arr), | ||
[Constants.FUNCS.BASE64_DIA]: () => el = () => this.getBase64Dia(is_reverse_base64), | ||
[Constants.FUNCS.TRANSLATE_1]: () => el = (arr, token) => this.translate1(arr, token, is_reverse_base64), | ||
[Constants.FUNCS.TRANSLATE_2]: () => el = (arr, token, base64_dic) => this.translate2(arr, token, base64_dic) | ||
})[this.getFunc(el)] || (() => el === 'b' && (el = n_token)))(); | ||
(({ // Identifies the transformation functions | ||
[Constants.FUNCS.PUSH]: () => el = (arr, i) => this.#push(arr, i), | ||
[Constants.FUNCS.SPLICE]: () => el = (arr, i) => this.#splice(arr, i), | ||
[Constants.FUNCS.SWAP0_1]: () => el = (arr, i) => this.#swap0(arr, i), | ||
[Constants.FUNCS.SWAP0_2]: () => el = (arr, i) => this.#swap0(arr, i), | ||
[Constants.FUNCS.ROTATE_1]: () => el = (arr, i) => this.#rotate(arr, i), | ||
[Constants.FUNCS.ROTATE_2]: () => el = (arr, i) => this.#rotate(arr, i), | ||
[Constants.FUNCS.REVERSE_1]: () => el = (arr) => this.#reverse(arr), | ||
[Constants.FUNCS.REVERSE_2]: () => el = (arr) => this.#reverse(arr), | ||
[Constants.FUNCS.BASE64_DIA]: () => el = () => this.#getBase64Dia(is_reverse_base64), | ||
[Constants.FUNCS.TRANSLATE_1]: () => el = (arr, token) => this.#translate1(arr, token, is_reverse_base64), | ||
[Constants.FUNCS.TRANSLATE_2]: () => el = (arr, token, base64_dic) => this.#translate2(arr, token, base64_dic) | ||
})[this.#getFunc(el)] || (() => el === 'b' && (el = n_token)))(); | ||
} | ||
@@ -40,8 +46,10 @@ return el; | ||
// Fills the null placeholders with a copy of the transformations array. | ||
let null_placeholder_positions = [...this.raw_code.matchAll(this.null_placeholder_regex)].map((item) => parseInt(item[1])); | ||
// Fills the null placeholders with a copy of the transformations array | ||
const null_placeholder_positions = [...this.raw_code.matchAll(this.null_placeholder_regex)].map((item) => parseInt(item[1])); | ||
null_placeholder_positions.forEach((pos) => transformations[pos] = transformations); | ||
// Parses and emulates calls to functions of the transformations array. | ||
let transformation_calls = [...Utils.getStringBetweenStrings(this.raw_code.replace(/\n/g, ''), 'try{', '}catch').matchAll(this.transformation_calls_regex)].map((params) => ({ index: params[1], params: params[2] })); | ||
// Parses and emulates calls to the functions of the transformations array | ||
const transformation_calls = [...Utils.getStringBetweenStrings(this.raw_code.replace(/\n/g, ''), 'try{', '}catch') | ||
.matchAll(this.transformation_calls_regex)].map((params) => ({ index: params[1], params: params[2] })); | ||
transformation_calls.forEach((data) => { | ||
@@ -53,14 +61,17 @@ const param_index = data.params.split(',').map((param) => param.match(/c\[(.*?)\]/)[1]); | ||
} catch (err) { | ||
console.error('Could not transform n-token, download may be throttled:', err); | ||
console.error(`Could not transform n-token (${n}), download may be throttled:`, err) | ||
return n; | ||
} | ||
return n_token.join(''); | ||
} | ||
getFunc(el) { | ||
#getFunc(el) { | ||
return el.match(Constants.FUNCS_REGEX); | ||
} | ||
getTransformationData() { | ||
/** | ||
* Takes the n-transform data, refines it, and then returns a readable json array. | ||
* @returns {object} | ||
*/ | ||
#getTransformationData() { | ||
const data = `[${Utils.getStringBetweenStrings(this.raw_code.replace(/\n/g, ''), 'c=[', '];c')}]`; | ||
@@ -70,5 +81,8 @@ return JSON.parse(Constants.formatNTransformData(data)); | ||
translate1(arr, token, is_reverse_base64) { | ||
/** | ||
* Gets a base64 alphabet and uses it as a lookup table to modify n. | ||
*/ | ||
#translate1(arr, token, is_reverse_base64) { | ||
const characters = is_reverse_base64 && Constants.BASE64_DIALECT.REVERSE || Constants.BASE64_DIALECT.NORMAL; | ||
arr.forEach(function(char, index, loc) { | ||
arr.forEach(function (char, index, loc) { | ||
this.push(loc[index] = characters[(characters.indexOf(char) - characters.indexOf(this[index]) + 64) % characters.length]); | ||
@@ -78,5 +92,5 @@ }, token.split('')); | ||
translate2(arr, token, characters) { | ||
#translate2(arr, token, characters) { | ||
let chars_length = characters.length; | ||
arr.forEach(function(char, index, loc) { | ||
arr.forEach(function (char, index, loc) { | ||
this.push(loc[index] = characters[(characters.indexOf(char) - characters.indexOf(this[index]) + index + chars_length--) % characters.length]); | ||
@@ -86,3 +100,6 @@ }, token.split('')); | ||
getBase64Dia(is_reverse_base64) { | ||
/** | ||
* Returns the requested base64 dialect, currently this is only used by 'translate2'. | ||
*/ | ||
#getBase64Dia(is_reverse_base64) { | ||
const characters = is_reverse_base64 && Constants.BASE64_DIALECT.REVERSE || Constants.BASE64_DIALECT.NORMAL; | ||
@@ -92,10 +109,16 @@ return characters; | ||
swap0(arr, index) { | ||
const old_value = arr[0]; | ||
/** | ||
* Swaps the first element with the one at the given index. | ||
*/ | ||
#swap0(arr, index) { | ||
const old_elem = arr[0]; | ||
index = (index % arr.length + arr.length) % arr.length; | ||
arr[0] = arr[index]; | ||
arr[index] = old_value; | ||
arr[index] = old_elem; | ||
} | ||
rotate(arr, index) { | ||
/** | ||
* Rotates elements of the array. | ||
*/ | ||
#rotate(arr, index) { | ||
index = (index % arr.length + arr.length) % arr.length; | ||
@@ -105,3 +128,6 @@ arr.splice(-index).reverse().forEach((el) => arr.unshift(el)); | ||
splice(arr, index) { | ||
/** | ||
* Deletes one element at the given index. | ||
*/ | ||
#splice(arr, index) { | ||
index = (index % arr.length + arr.length) % arr.length; | ||
@@ -111,7 +137,7 @@ arr.splice(index, 1); | ||
reverse(arr) { | ||
#reverse(arr) { | ||
arr.reverse(); | ||
} | ||
push(arr, item) { | ||
#push(arr, item) { | ||
arr.push(item); | ||
@@ -118,0 +144,0 @@ } |
'use strict'; | ||
const Axios = require('axios'); | ||
const Utils = require('./Utils'); | ||
const Constants = require('./Constants'); | ||
@@ -12,2 +11,3 @@ const EventEmitter = require('events'); | ||
super(); | ||
this.auth_info = auth_info; | ||
this.refresh_interval = 5; | ||
@@ -24,10 +24,13 @@ | ||
this.auth_script_regex = /<script id=\"base-js\" src=\"(.*?)\" nonce=".*?"><\/script>/; | ||
this.identity_regex = /var .+?=\"(?<id>.+?)\",.+?=\"(?<secret>.+?)\"/; | ||
this.identity_regex = /.+?={};var .+?={clientId:\"(?<id>.+?)\",si:\"(?<secret>.+?)\"},/; | ||
if (auth_info.access_token) return; | ||
this.requestAuthCode(); | ||
this.#requestAuthCode(); | ||
} | ||
async requestAuthCode() { | ||
const identity = await this.getClientIdentity(); | ||
/** | ||
* Asks the OAuth server for an auth code. | ||
*/ | ||
async #requestAuthCode() { | ||
const identity = await this.#getClientIdentity(); | ||
@@ -45,3 +48,3 @@ this.client_id = identity.id; | ||
const response = await Axios.post(this.oauth_code_url, JSON.stringify(data), Constants.OAUTH.HEADERS).catch((error) => error); | ||
if (response instanceof Error) | ||
@@ -62,7 +65,11 @@ return this.emit('auth', { | ||
// Keeps requesting at a specific rate until the authorization is granted or denied. | ||
this.waitForAuth(response.data.device_code); | ||
this.#waitForAuth(response.data.device_code); | ||
} | ||
waitForAuth(device_code) { | ||
/** | ||
* Waits for sign-in authorization. | ||
* | ||
* @param {string} device_code Client's device code. | ||
*/ | ||
#waitForAuth(device_code) { | ||
const data = { | ||
@@ -87,3 +94,3 @@ client_id: this.client_id, | ||
case 'authorization_pending': | ||
this.waitForAuth(device_code); | ||
this.#waitForAuth(device_code); | ||
break; | ||
@@ -101,3 +108,3 @@ case 'access_denied': | ||
}); | ||
this.requestAuthCode(); | ||
this.#requestAuthCode(); | ||
break; | ||
@@ -108,3 +115,3 @@ default: | ||
const expiration_date = new Date(new Date().getTime() + response.data.expires_in * 1000); | ||
this.emit('auth', { | ||
@@ -123,4 +130,8 @@ credentials: { | ||
async refreshAccessToken(refresh_token) { | ||
const identity = await this.getClientIdentity(); | ||
/** | ||
* Gets a new access token using a refresh token. | ||
* @returns {object} { credentials: { access_token: string, refresh_token: string, expires: string }, status: 'FAILED' | 'SUCCESS' } | ||
*/ | ||
async refreshAccessToken() { | ||
const identity = await this.#getClientIdentity(); | ||
@@ -130,6 +141,6 @@ const data = { | ||
client_secret: identity.secret, | ||
refresh_token, | ||
refresh_token: this.auth_info.refresh_token, | ||
grant_type: 'refresh_token', | ||
}; | ||
const response = await Axios.post(this.oauth_token_url, JSON.stringify(data), Constants.OAUTH.HEADERS).catch((error) => error); | ||
@@ -141,3 +152,3 @@ if (response instanceof Error) { | ||
}); | ||
return { | ||
@@ -149,11 +160,11 @@ credentials: { | ||
}, | ||
status: 'FAILED' | ||
status: 'FAILED' | ||
}; | ||
} | ||
const expiration_date = new Date(new Date().getTime() + response.data.expires_in * 1000); | ||
return { | ||
credentials: { | ||
refresh_token: refresh_token, | ||
refresh_token: this.auth_info.refresh_token, | ||
access_token: response.data.access_token, | ||
@@ -166,15 +177,13 @@ expires: expiration_date | ||
} | ||
async isTokenValid(expiration_date) { | ||
const timestamp = new Date(expiration_date).getTime(); | ||
const is_valid = new Date().getTime() < timestamp; | ||
return is_valid; | ||
} | ||
async getClientIdentity() { | ||
// The first request is made to get the auth script url, hard-coding it isn't viable as it changes overtime. | ||
/** | ||
* Gets client identity data. | ||
* @returns {object} { id: string, secret: string } | ||
*/ | ||
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); | ||
if (yttv_response instanceof Error) throw new Error(`Could not extract client identity: ${yttv_response.message}`); | ||
// Here we get the script and extract the necessary data to proceed with the auth flow. | ||
// 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]; | ||
@@ -186,9 +195,17 @@ const script_url = `${Constants.URLS.YT_BASE_URL}/${url_body}`; | ||
const identity_function = Utils.getStringBetweenStrings(response.data, 'setQuery("");', '{useGaiaSandbox:'); | ||
const client_identity = identity_function.replace(/\n/g, '').match(this.identity_regex); | ||
const client_identity = response.data.replace(/\n/g, '').match(this.identity_regex); | ||
return client_identity.groups; | ||
} | ||
/** | ||
* Checks access token validity. | ||
* @returns {boolean} true | false | ||
*/ | ||
isTokenValid() { | ||
const timestamp = new Date(this.auth_info.expires).getTime(); | ||
const is_valid = new Date().getTime() < timestamp; | ||
return is_valid; | ||
} | ||
} | ||
module.exports = OAuth; |
'use strict'; | ||
const fs = require('fs'); | ||
const Fs = require('fs'); | ||
const Axios = require('axios'); | ||
@@ -9,4 +9,4 @@ const Utils = require('./Utils'); | ||
class Player { | ||
constructor(innertube_session) { | ||
this.session = innertube_session; | ||
constructor(session) { | ||
this.session = session; | ||
this.player_name = Utils.getStringBetweenStrings(this.session.player_url, '/player/', '/'); | ||
@@ -17,6 +17,6 @@ this.tmp_cache_dir = __dirname.slice(0, -3) + 'cache'; | ||
async init() { | ||
if (fs.existsSync(`${this.tmp_cache_dir}/${this.player_name}.js`)) { | ||
const player_data = fs.readFileSync(`${this.tmp_cache_dir}/${this.player_name}.js`).toString(); | ||
this.getSigDecipherCode(player_data); | ||
this.getNEncoder(player_data); | ||
if (Fs.existsSync(`${this.tmp_cache_dir}/${this.player_name}.js`)) { | ||
const player_data = Fs.readFileSync(`${this.tmp_cache_dir}/${this.player_name}.js`).toString(); | ||
this.sig_decipher_sc = this.#getSigDecipherCode(player_data); | ||
this.ntoken_sc = this.#getNEncoder(player_data); | ||
} else { | ||
@@ -27,20 +27,20 @@ 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); | ||
try { | ||
// Caches the current player so we don't have to download it all the time | ||
fs.mkdirSync(this.tmp_cache_dir, { recursive: true }); | ||
fs.writeFileSync(`${this.tmp_cache_dir}/${this.player_name}.js`, response.data); | ||
// Caches the current player so we don't have to download it all the time. | ||
Fs.mkdirSync(this.tmp_cache_dir, { recursive: true }); | ||
Fs.writeFileSync(`${this.tmp_cache_dir}/${this.player_name}.js`, response.data); | ||
} catch (err) {} | ||
this.getSigDecipherCode(response.data); | ||
this.getNEncoder(response.data); | ||
this.sig_decipher_sc = this.#getSigDecipherCode(response.data); | ||
this.ntoken_sc = this.#getNEncoder(response.data); | ||
} | ||
} | ||
getSigDecipherCode(data) { | ||
const manipulation_algorithm_code = Utils.getStringBetweenStrings(data, 'this.audioTracks};var', '};'); | ||
const manipulation_sequence_code = Utils.getStringBetweenStrings(data, 'function(a){a=a.split("")', 'return a.join("")}'); | ||
this.sig_decipher_sc = manipulation_algorithm_code + manipulation_sequence_code; | ||
#getSigDecipherCode(data) { | ||
const sig_alg_sc = Utils.getStringBetweenStrings(data, 'this.audioTracks};var', '};'); | ||
const sig_data = Utils.getStringBetweenStrings(data, 'function(a){a=a.split("")', 'return a.join("")}'); | ||
return sig_alg_sc + sig_data; | ||
} | ||
getNEncoder(data) { | ||
this.ntoken_sc = `var b=a.split("")${Utils.getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}')}} return b.join("");`; | ||
#getNEncoder(data) { | ||
return `var b=a.split("")${Utils.getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}')}} return b.join("");`; | ||
} | ||
@@ -47,0 +47,0 @@ } |
@@ -15,5 +15,8 @@ 'use strict'; | ||
/** | ||
* Deciphers signature. | ||
*/ | ||
decipher() { | ||
const args = QueryString.parse(this.url); | ||
const functions = this.getFunctions(); | ||
const functions = this.#getFunctions(); | ||
@@ -60,3 +63,3 @@ function splice(arr, end) { | ||
getFunctions() { | ||
#getFunctions() { | ||
let func; | ||
@@ -63,0 +66,0 @@ let func_name = []; |
@@ -8,2 +8,7 @@ 'use strict'; | ||
/** | ||
* Returns a random user agent. | ||
* | ||
* @param {string} type mobile | desktop | ||
*/ | ||
function getRandomUserAgent(type) { | ||
@@ -19,2 +24,7 @@ switch (type) { | ||
/** | ||
* Generates an authentication token from a cookies' sid. | ||
* | ||
* @param {string} sid Sid extracted from cookies | ||
*/ | ||
function generateSidAuth(sid) { | ||
@@ -32,2 +42,10 @@ const youtube = 'https://www.youtube.com'; | ||
/** | ||
* Gets a string between two delimiters. | ||
* | ||
* @param {string} data The data. | ||
* @param {string} start_string Start string. | ||
* @param {string} end_string End string. | ||
*/ | ||
function getStringBetweenStrings(data, start_string, end_string) { | ||
@@ -43,2 +61,32 @@ const regex = new RegExp(`${escapeStringRegexp(start_string)}(.*?)${escapeStringRegexp(end_string)}`, "s"); | ||
/** | ||
* Converts time (h:m:s) to seconds. | ||
* | ||
* @param {string} time | ||
* @returns {string} seconds | ||
*/ | ||
function timeToSeconds(time) { | ||
let params = time.split(':'); | ||
return parseInt(({ | ||
3: +params[0] * 3600 + +params[1] * 60 + +params[2], | ||
2: +params[0] * 60 + +params[1], | ||
1: +params[0] | ||
})[params.length]); | ||
} | ||
/** | ||
* Converts strings in camelCase to snake_case. | ||
* | ||
* @param {string} string The string in camelCase. | ||
*/ | ||
function camelToSnake(string) { | ||
return string[0].toLowerCase() + string.slice(1, string.length).replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); | ||
} | ||
/** | ||
* Encodes notification preferences protobuf. | ||
* | ||
* @param {string} channel_id | ||
* @param {string} index | ||
*/ | ||
function encodeNotificationPref(channel_id, index) { | ||
@@ -59,3 +107,10 @@ const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/proto/youtube.proto`)); | ||
function generateMessageParams(channel_id, video_id) { | ||
/** | ||
* Encodes livestream message protobuf. | ||
* | ||
* @param {string} channel_id | ||
* @param {string} video_id | ||
*/ | ||
function encodeMessageParams(channel_id, video_id) { | ||
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/proto/youtube.proto`)); | ||
@@ -77,3 +132,9 @@ | ||
function generateCommentParams(video_id) { | ||
/** | ||
* Encodes comment params protobuf. | ||
* | ||
* @param {string} video_id | ||
*/ | ||
function encodeCommentParams(video_id) { | ||
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/proto/youtube.proto`)); | ||
@@ -92,2 +153,10 @@ | ||
/** | ||
* Encodes search filter protobuf | ||
* | ||
* @param {string} period Period in which a video is uploaded: any | hour | day | week | month | year | ||
* @param {string} duration The duration of a video: any | short | long | ||
* @param {string} order The order of the search results: relevance | rating | age | views | ||
*/ | ||
function encodeFilter(period, duration, order) { | ||
@@ -112,2 +181,2 @@ const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/proto/youtube.proto`)); | ||
module.exports = { getRandomUserAgent, generateSidAuth, getStringBetweenStrings, generateMessageParams, generateCommentParams, encodeNotificationPref, encodeFilter }; | ||
module.exports = { getRandomUserAgent, generateSidAuth, getStringBetweenStrings, camelToSnake, timeToSeconds, encodeMessageParams, encodeCommentParams, encodeNotificationPref, encodeFilter }; |
{ | ||
"name": "youtubei.js", | ||
"version": "1.2.8", | ||
"version": "1.2.9", | ||
"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!", | ||
@@ -18,3 +18,2 @@ "main": "index.js", | ||
"protons": "^2.0.3", | ||
"time-to-seconds": "^1.1.5", | ||
"user-agents": "^1.0.778", | ||
@@ -30,2 +29,3 @@ "uuid": "^8.3.2" | ||
"youtube-dl", | ||
"youtubedl", | ||
"innertube", | ||
@@ -39,3 +39,2 @@ "innertubeapi", | ||
"comment", | ||
"automation", | ||
"downloader", | ||
@@ -42,0 +41,0 @@ "comments-section", |
@@ -1,12 +0,18 @@ | ||
# YouTube.js | ||
<h1 align="center">YouTube.js</h1> | ||
<p align="center"><i>An object-oriented wrapper around the Innertube API, which is what YouTube itself uses.</i><p> | ||
[![Build](https://github.com/LuanRT/YouTube.js/actions/workflows/node.js.yml/badge.svg)](https://github.com/LuanRT/YouTube.js/actions/workflows/node.js.yml) | ||
[![NPM](https://img.shields.io/npm/v/youtubei.js?color=%2335C757)](https://www.npmjs.com/package/youtubei.js) | ||
[![CodeFactor](https://www.codefactor.io/repository/github/luanrt/youtube.js/badge)](https://www.codefactor.io/repository/github/luanrt/youtube.js) | ||
<p align="center"> | ||
<img src="https://github.com/LuanRT/YouTube.js/actions/workflows/node.js.yml/badge.svg"> | ||
<img src="https://img.shields.io/npm/v/youtubei.js?color=%2335C757"> | ||
<img src="https://www.codefactor.io/repository/github/luanrt/youtube.js/badge"> | ||
</p> | ||
An object-oriented wrapper around the Innertube API, which is what YouTube itself uses. This makes YouTube.js fast, simple & efficient. And big thanks to [@gatecrasher777](https://github.com/gatecrasher777/ytcog) for his research on the workings of the Innertube API! | ||
Innertube is an API used across all YouTube clients, it was [made to simplify](https://gizmodo.com/how-project-innertube-helped-pull-youtube-out-of-the-gu-1704946491) the internal structure of the platform and make it easy to push updates. This library takes advantage of that API, therefore providing a simple & efficient way to interact with YouTube programmatically. | ||
And big thanks to [@gatecrasher777](https://github.com/gatecrasher777/ytcog) for his research on the workings of the Innertube API! | ||
#### What can it do? | ||
As of now, this is one of the most advanced & stable YouTube libraries out there, here's a short summary of what it can do: | ||
As of now, this is one of the most advanced & stable YouTube libraries out there, here's a short summary of its features: | ||
@@ -16,8 +22,8 @@ - Search videos | ||
- Fetch live chat & live stats in real time | ||
- Fetch notifications | ||
- Fetch subscriptions feed | ||
- Change notifications preferences for a channel | ||
- Subscribe/Unsubscribe/Like/Dislike/Comment, etc | ||
- Easily sign into your account in an easy & reliable way. | ||
- Last but not least, you can also download videos! | ||
- Get notifications | ||
- Get subscriptions feed | ||
- Change notification preferences for a channel | ||
- Subscribe/Unsubscribe/Like/Dislike/Comment etc | ||
- Easily sign in to any Google Account | ||
- Download videos | ||
@@ -38,29 +44,21 @@ Do note that you must be signed-in to perform actions that involve an account, such as commenting, subscribing, sending messages to a live chat, etc. | ||
[1. Basic Usage](https://www.npmjs.com/package/youtubei.js#usage) | ||
[1. Basic Usage](https://github.com/LuanRT/YouTube.js#usage) | ||
[2. Interactions](https://www.npmjs.com/package/youtubei.js#interactions) | ||
[2. Interactions](https://github.com/LuanRT/YouTube.js#interactions) | ||
[3. Fetching live chats](https://www.npmjs.com/package/youtubei.js#fetching-live-chats) | ||
[3. Live chats](https://github.com/LuanRT/YouTube.js#fetching-live-chats) | ||
[4. Downloading videos](https://www.npmjs.com/package/youtubei.js#downloading-videos) | ||
[4. Downloading videos](https://github.com/LuanRT/YouTube.js#downloading-videos) | ||
[5. Signing-in](https://www.npmjs.com/package/youtubei.js#signing-in) | ||
[5. Signing-in](https://github.com/LuanRT/YouTube.js#signing-in) | ||
[6. Disclaimer](https://www.npmjs.com/package/youtubei.js#disclaimer) | ||
[6. Disclaimer](https://github.com/LuanRT/YouTube.js#disclaimer) | ||
First of all we're gonna start by initializing the Innertube class: | ||
First of all we're gonna start by initializing the Innertube instance. | ||
And to make things faster, you should do this only once and reuse the Innertube object when needed. | ||
```js | ||
const Innertube = require('youtubei.js'); | ||
async function start() { | ||
const youtube = await new Innertube(); | ||
//... | ||
} | ||
start(); | ||
const youtube = await new Innertube(); | ||
``` | ||
After this you should be good to go, so let's dive into it! | ||
Doing a simple search: | ||
@@ -494,2 +492,8 @@ | ||
Get unseen notifications count: | ||
```js | ||
const notifications = await youtube.getUnseenNotificationsCount(); | ||
``` | ||
### Interactions: | ||
@@ -501,3 +505,3 @@ | ||
```js | ||
const video = await youtube.getDetails(VIDEO_ID_HERE); // this is equivalent to opening the watch page on YouTube | ||
const video = await youtube.getDetails(VIDEO_ID_HERE); | ||
@@ -510,3 +514,3 @@ await video.subscribe(); | ||
```js | ||
const video = await youtube.getDetails(VIDEO_ID_HERE); // this is equivalent to opening the watch page on YouTube | ||
const video = await youtube.getDetails(VIDEO_ID_HERE); | ||
@@ -528,3 +532,3 @@ await video.like(); | ||
// Can be: ALL | NONE | PERSONALIZED | ||
// Options: ALL | NONE | PERSONALIZED | ||
await video.setNotificationPref('ALL'); | ||
@@ -547,3 +551,2 @@ ``` | ||
// This should only be called if you're sure it's a livestream and that it's still ongoing | ||
const livechat = video.getLivechat(); | ||
@@ -573,3 +576,3 @@ | ||
Deleting a message: | ||
Delete a message: | ||
```js | ||
@@ -591,3 +594,5 @@ const msg = await livechat.sendMessage('Nice livestream!'); | ||
const youtube = await new Innertube(); | ||
const search = await youtube.search('Looking for life on Mars - documentary'); | ||
const stream = youtube.download(search.videos[0].id, { | ||
@@ -628,3 +633,3 @@ format: 'mp4', // Optional, ignored when type is set to audio and defaults to mp4, and I recommend to leave it as it is | ||
You can also download only a portion of a video by specifying a range: | ||
You can also specify a range: | ||
```js | ||
@@ -647,6 +652,6 @@ const stream = youtube.download(VIDEO_ID, { | ||
This library allows you to sign-in in two different ways: | ||
When signing in to your account, you have two options: | ||
- Using OAuth 2.0, easy, simple & reliable. | ||
- Cookies, usually more complicated to get and unreliable. | ||
- Use OAuth 2.0; easy, simple & reliable. | ||
- Cookies; usually more complicated to get and unreliable. | ||
@@ -699,5 +704,2 @@ OAuth: | ||
## Note | ||
Never sign-in with your personal account, you might get banned if you spam (don't ever do that) or simply because YouTube detected unusual activity coming from your account. Also, I'm not responsible if any of that happens to you. | ||
## Contributing | ||
@@ -704,0 +706,0 @@ Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. |
@@ -9,4 +9,3 @@ 'use strict'; | ||
client_version: '2.20211101.01.00', | ||
test_video_id: 'FT_nzxtgXEw', | ||
test_video_id_1: 'YE7VzlLtp-4', | ||
test_video_id: 'dQw4w9WgXcQ', | ||
sig_decipher_sc: `fB={RP:function(a,b){a.splice(0,b)}, | ||
@@ -13,0 +12,0 @@ Td:function(a){a.reverse()}, |
@@ -17,14 +17,15 @@ 'use strict'; | ||
const search = await youtube.search('Carl Sagan - Documentary').catch((error) => error); | ||
assert((search instanceof Error ? false : true) && search.videos.length >= 1, `should search videos`, search); | ||
assert(!(search instanceof Error) && search.videos.length >= 1, `should search videos`, search); | ||
const details = await youtube.getDetails(Constants.test_video_id).catch((error) => error); | ||
assert(details instanceof Error ? false : true, `should retrieve details for ${Constants.test_video_id}`, details); | ||
assert(!(details instanceof Error), `should retrieve details for ${Constants.test_video_id}`, details); | ||
const comments = await youtube.getComments(Constants.test_video_id).catch((error) => error); | ||
assert(comments instanceof Error ? false : true, `should retrieve comments for ${Constants.test_video_id}`, comments); | ||
assert(!(comments instanceof Error), `should retrieve comments for ${Constants.test_video_id}`, comments); | ||
const video = await downloadVideo(Constants.test_video_id_1, youtube).catch((error) => error); | ||
assert(video instanceof Error ? false : true, `should download video (${Constants.test_video_id_1})`, video); | ||
const video = await downloadVideo(Constants.test_video_id, youtube).catch((error) => error); | ||
assert(!(video instanceof Error), `should download video (${Constants.test_video_id})`, video); | ||
} | ||
const n_token = new NToken(Constants.n_scramble_sc).transform(Constants.original_ntoken); | ||
@@ -53,4 +54,7 @@ assert(n_token == Constants.expected_ntoken, `should transform n token into ${Constants.expected_ntoken}`, n_token); | ||
const pass_fail = outcome ? 'pass' : 'fail'; | ||
console.info(pass_fail, ':', description); | ||
!outcome && (failed_tests += 1); | ||
console.info(pass_fail, ':', description, !outcome && `\nError: ${data}` || ''); | ||
!outcome && console.error('Error: ', data); | ||
return outcome; | ||
@@ -57,0 +61,0 @@ } |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
113266
4
21
1784
705
4
- Removedtime-to-seconds@^1.1.5
- Removedtime-to-seconds@1.8.0(transitive)