youtubei.js
Advanced tools
Comparing version 1.2.7 to 1.2.8-npm
@@ -9,4 +9,5 @@ 'use strict'; | ||
async function engage(session, engagement_type, args = {}) { | ||
if (!session.logged_in) throw new Error('You are not logged in'); | ||
let data = {}; | ||
if (!session.logged_in) throw new Error('You are not signed-in'); | ||
let data; | ||
switch (engagement_type) { | ||
@@ -40,4 +41,7 @@ case 'like/like': | ||
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 { | ||
@@ -50,5 +54,6 @@ success: true, | ||
async function browse(session, action_type) { | ||
if (!session.logged_in) throw new Error('You are not logged in'); | ||
if (!session.logged_in) throw new Error('You are not signed-in'); | ||
let data; | ||
switch (action_type) { | ||
switch (action_type) { // TODO: Handle more actions | ||
case 'subscriptions_feed': | ||
@@ -63,4 +68,7 @@ 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 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 { | ||
@@ -73,5 +81,25 @@ success: true, | ||
async function search(session, 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); | ||
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 | ||
}; | ||
} | ||
async function notifications(session, action_type, args = {}) { | ||
if (!session.logged_in) throw new Error('You are not logged in'); | ||
if (!session.logged_in) throw new Error('You are not signed-in'); | ||
let data; | ||
switch (action_type) { | ||
@@ -99,5 +127,7 @@ case 'modify_channel_preference': | ||
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 { | ||
@@ -137,4 +167,6 @@ success: true, | ||
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 { | ||
@@ -147,2 +179,13 @@ success: true, | ||
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)); | ||
if (response instanceof Error) throw new Error(`Could not get video info: ${response.message}`); | ||
return response.data; | ||
} | ||
async function getContinuation(session, info = {}) { | ||
@@ -164,4 +207,6 @@ let data = { context: session.context }; | ||
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 { | ||
@@ -174,2 +219,2 @@ success: true, | ||
module.exports = { engage, browse, notifications, livechat, getContinuation }; | ||
module.exports = { engage, browse, search, notifications, livechat, getVideoInfo, getContinuation }; |
@@ -144,2 +144,3 @@ 'use strict'; | ||
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, '')))]; | ||
@@ -173,2 +174,3 @@ video_details.id = data.videoDetails.videoId; | ||
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, '')))]; | ||
@@ -180,3 +182,3 @@ video_details.id = data[2].playerResponse.videoDetails.videoId; | ||
// Functions | ||
// Placeholders for functions | ||
video_details.like = () => {}; | ||
@@ -196,79 +198,3 @@ video_details.dislike = () => {}; | ||
return video_details; | ||
}, | ||
filters: (order) => { | ||
return (({ | ||
'any,any,relevance': 'EgIQAQ%3D%3D', | ||
'hour,any,relevance': 'EgIIAQ%3D%3D', | ||
'day,any,relevance': 'EgQIAhAB', | ||
'week,any,relevance': 'EgQIAxAB', | ||
'month,any,relevance': 'EgQIBBAB', | ||
'year,any,relevance': 'EgQIBRAB', | ||
'any,short,relevance': 'EgQQARgB', | ||
'hour,short,relevance': 'EgYIARABGAE%3D', | ||
'day,short,relevance': 'EgYIAhABGAE%3D', | ||
'week,short,relevance': 'EgYIAxABGAE%3D', | ||
'month,short,relevance': 'EgYIBBABGAE%3D', | ||
'year,short,relevance': 'EgYIBRABGAE%3D', | ||
'any,long,relevance': 'EgQQARgC', | ||
'hour,long,relevance': 'EgYIARABGAI%3D', | ||
'day,long,relevance': 'EgYIAhABGAI%3D', | ||
'week,long,relevance': 'EgYIAxABGAI%3D', | ||
'month,long,relevance': 'EgYIBBABGAI%3D', | ||
'year,long,relevance': 'EgYIBRABGAI%3D', | ||
'any,any,age': 'CAISAhAB', | ||
'hour,any,age': 'CAISBAgBEAE%3D', | ||
'day,any,age': 'CAISBAgCEAE%3D', | ||
'week,any,age': 'CAISBAgDEAE%3D', | ||
'month,any,age': 'CAISBAgEEAE%3D', | ||
'year,any,age': 'CAISBAgFEAE%3D', | ||
'any,short,age': 'CAISBBABGAE%3D', | ||
'hour,short,age': 'CAISBggBEAEYAQ%3D%3D', | ||
'day,short,age': 'CAISBggCEAEYAQ%3D%3D', | ||
'week,short,age': 'CAISBggDEAEYAQ%3D%3D', | ||
'month,short,age': 'CAISBggEEAEYAQ%3D%3D', | ||
'year,short,age': 'CAISBggFEAEYAQ%3D%3D', | ||
'any,long,age': 'CAISBBABGAI%3D', | ||
'hour,long,age': 'CAISBggBEAEYAg%3D%3D', | ||
'day,long,age': 'CAISBggCEAEYAg%3D%3D', | ||
'week,long,age': 'CAISBggDEAEYAg%3D%3D', | ||
'month,long,age': 'CAISBggEEAEYAg%3D%3D', | ||
'year,long,age': 'CAISBggFEAEYAg%3D%3D', | ||
'any,any,views': 'CAMSAhAB', | ||
'hour,any,views': 'CAMSBAgBEAE%3D', | ||
'day,any,views': 'CAMSBAgCEAE%3D', | ||
'week,any,views': 'CAMSBAgDEAE%3D', | ||
'month,any,views': 'CAMSBAgEEAE%3D', | ||
'year,any,views': 'CAMSBAgFEAE%3D', | ||
'any,short,views': 'CAMSBBABGAE%3D', | ||
'hour,short,views': 'CAMSBggBEAEYAQ%3D%3D', | ||
'day,short,views': 'CAMSBggCEAEYAQ%3D%3D', | ||
'week,short,views': 'CAMSBggDEAEYAQ%3D%3D', | ||
'month,short,views': 'CAMSBggEEAEYAQ%3D%3D', | ||
'year,short,views': 'CAMSBggFEAEYAQ%3D%3D', | ||
'any,long,views': 'CAMSBBABGAI%3D', | ||
'hour,long,views': 'CAMSBggBEAEYAg%3D%3D', | ||
'day,long,views': 'CAMSBggCEAEYAg%3D%3D', | ||
'week,long,views': 'CAMSBggDEAEYAg%3D%3D', | ||
'month,long,views': 'CAMSBggEEAEYAg%3D%3D', | ||
'year,long,views': 'CAMSBggFEAEYAg%3D%3D', | ||
'any,any,rating': 'CAESAhAB', | ||
'hour,any,rating': 'CAESBAgBEAE%3D', | ||
'day,any,rating': 'CAESBAgCEAE%3D', | ||
'week,any,rating': 'CAESBAgDEAE%3D', | ||
'month,any,rating': 'CAESBAgEEAE%3D', | ||
'year,any,rating': 'CAESBAgFEAE%3D', | ||
'any,short,rating': 'CAESBBABGAE%3D', | ||
'hour,short,rating': 'CAESBggBEAEYAQ%3D%3D', | ||
'day,short,rating': 'CAESBggCEAEYAQ%3D%3D', | ||
'week,short,rating': 'CAESBggDEAEYAQ%3D%3D', | ||
'month,short,rating': 'CAESBggEEAEYAQ%3D%3D', | ||
'year,short,rating': 'CAESBggFEAEYAQ%3D%3D', | ||
'any,long,rating': 'CAESBBABGAI%3D', | ||
'hour,long,rating': 'CAESBggBEAEYAg%3D%3D', | ||
'day,long,rating': 'CAESBggCEAEYAg%3D%3D', | ||
'week,long,rating': 'CAESBggDEAEYAg%3D%3D', | ||
'month,long,rating': 'CAESBggEEAEYAg%3D%3D', | ||
'year,long,rating': 'CAESBggFEAEYAg%3D%3D' | ||
})[order] || 'EgIQAQ%3D%3D'); | ||
} | ||
}; |
@@ -17,5 +17,4 @@ 'use strict'; | ||
class Innertube extends EventEmitter { | ||
class Innertube { | ||
constructor(cookie) { | ||
super(); | ||
this.cookie = cookie || ''; | ||
@@ -50,2 +49,4 @@ this.retry_count = 0; | ||
} | ||
this.ev = new EventEmitter(); | ||
} else { | ||
@@ -64,36 +65,39 @@ this.retry_count += 1; | ||
signIn(credentials = {}) { | ||
signIn(auth_info = {}) { | ||
return new Promise(async (resolve, reject) => { | ||
const oauth = new OAuth(credentials); | ||
if (credentials.access_token && credentials.refresh_token) { | ||
let token_validity = await oauth.checkTokenValidity(credentials.access_token, this); | ||
if (token_validity === 'VALID') { | ||
this.access_token = credentials.access_token; | ||
this.refresh_token = credentials.refresh_token; | ||
this.logged_in = true; | ||
resolve(); | ||
} else { | ||
oauth.refreshAccessToken(credentials.refresh_token); | ||
oauth.on('refresh-token', (data) => { | ||
this.access_token = data.access_token; | ||
this.refresh_token = credentials.refresh_token; | ||
this.logged_in = true; | ||
this.emit('update-credentials', { | ||
access_token: data.access_token, | ||
refresh_token: credentials.refresh_token, | ||
status: data.status | ||
}); | ||
resolve(); | ||
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); | ||
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.access_token = auth_info.access_token; | ||
this.refresh_token = auth_info.refresh_token; | ||
this.logged_in = true; | ||
resolve(); | ||
} else { | ||
oauth.on('auth', (data) => { | ||
if (data.status === 'SUCCESS') { | ||
this.emit('auth', data); | ||
this.access_token = data.access_token; | ||
this.refresh_token = data.refresh_token; | ||
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.emit('auth', data); | ||
this.ev.emit('auth', data); | ||
} | ||
@@ -106,17 +110,15 @@ }); | ||
async search(query, options = { period: 'any', order: 'relevance', duration: 'any' }) { | ||
if (!query) throw new Error('No query was provided'); | ||
const response = await Actions.search(this, { 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 response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/search${this.logged_in && this.cookie.length < 1 ? '' : `?key=${this.key}`}`, JSON.stringify({ context: this.context, params: Constants.filters(options.period + ',' + options.duration + ',' + options.order), query }), Constants.INNERTUBE_REQOPTS({ session: this })).catch((error) => error); | ||
if (response instanceof Error) throw new Error(`Could not search on YouTube: ${response.message}`); | ||
let content = response.data.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents[0].itemSectionRenderer.contents; | ||
let search_response = {}; | ||
search_response.search_metadata = {}; | ||
search_response.search_metadata.query = content[0].showingResultsForRenderer ? content[0].showingResultsForRenderer.originalQuery.simpleText : query; | ||
search_response.search_metadata.corrected_query = content[0].showingResultsForRenderer ? content[0].showingResultsForRenderer.correctedQueryEndpoint.searchEndpoint.query : query; | ||
search_response.search_metadata.estimated_results = parseInt(response.data.estimatedResults); | ||
search_response.videos = content.map((data) => { | ||
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; | ||
let video = data.videoRenderer; | ||
const video = data.videoRenderer; | ||
return { | ||
@@ -147,9 +149,9 @@ title: video.title.runs[0].text, | ||
}).filter((video_block) => video_block !== undefined); | ||
return search_response; | ||
return search; | ||
} | ||
async getDetails(id) { | ||
if (!id) return { error: 'Missing video id' }; | ||
if (!id) throw new Error('You must provide a video id'); | ||
const data = await this.requestVideoInfo(id, false); | ||
const data = await Actions.getVideoInfo(this, { id, is_desktop: false }); | ||
const video_data = Constants.formatVideoData(data, this, false); | ||
@@ -291,10 +293,2 @@ | ||
async requestVideoInfo(id, desktop) { | ||
let response; | ||
!desktop && (response = await Axios.get(`${Constants.URLS.YT_WATCH_PAGE}?v=${id}&t=8s&pbj=1`, Constants.INNERTUBE_REQOPTS({ session: this, id, desktop: false })).catch((error) => error)) || | ||
(response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/player${this.logged_in && this.cookie.length < 1 ? '' : `?key=${this.key}`}`, JSON.stringify(Constants.VIDEO_INFO_REQBODY(id, this.sts, this.context)), Constants.INNERTUBE_REQOPTS({ session: this, id, desktop: true })).catch((error) => error)); | ||
if (response instanceof Error) throw new Error('Could not retrieve watch page info: ' + response.message); | ||
return response.data; | ||
} | ||
download(id, options = {}) { | ||
@@ -311,3 +305,3 @@ if (!id) throw new Error('Missing video id'); | ||
const stream = new Stream.PassThrough(); | ||
this.requestVideoInfo(id, true).then(async (video_data) => { | ||
Actions.getVideoInfo(this, { id, is_desktop: true }).then(async (video_data) => { | ||
let formats = []; | ||
@@ -397,2 +391,3 @@ | ||
let downloaded_size = 0; | ||
response.data.on('data', (chunk) => { | ||
@@ -406,7 +401,4 @@ downloaded_size += chunk.length; | ||
response.data.on('error', (err) => { | ||
if (cancelled) { | ||
stream.emit('error', { message: 'The download was cancelled.', type: 'DOWNLOAD_CANCELLED' }); | ||
} else { | ||
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' }); | ||
}); | ||
@@ -413,0 +405,0 @@ |
106
lib/OAuth.js
@@ -7,18 +7,13 @@ 'use strict'; | ||
const EventEmitter = require('events'); | ||
const Uuid = require("uuid"); | ||
const Uuid = require('uuid'); | ||
class OAuth extends EventEmitter { | ||
constructor(creds) { | ||
constructor(auth_info) { | ||
super(); | ||
// Default interval between requests when waiting for authorization. | ||
this.refresh_interval = 5; | ||
// OAuth URLs: | ||
this.oauth_code_url = `${Constants.URLS.YT_BASE_URL}/o/oauth2/device/code`; | ||
this.oauth_token_url = `${Constants.URLS.YT_BASE_URL}/o/oauth2/token`; | ||
// Used to check whether an access token is valid or not. | ||
this.guide_url = `${Constants.URLS.YT_BASE_URL}/youtubei/v1/guide`; | ||
// These are always the same, so we shouldn't have any problems for now. | ||
this.model_name = Constants.OAUTH.MODEL_NAME; | ||
@@ -28,9 +23,6 @@ this.grant_type = Constants.OAUTH.GRANT_TYPE; | ||
// Script that contains important information such as client id and client secret. | ||
this.auth_script_regex = /<script id=\"base-js\" src=\"(.*?)\" nonce=".*?"><\/script>/; | ||
// Used to find the credentials inside the script. | ||
this.identity_regex = /var .+?=\"(?<id>.+?)\",.+?=\"(?<secret>.+?)\"/; | ||
if (creds.access_token != undefined && creds.refresh_token != undefined) return; | ||
if (auth_info.access_token) return; | ||
this.requestAuthCode(); | ||
@@ -41,2 +33,3 @@ } | ||
const identity = await this.getClientIdentity(); | ||
this.client_id = identity.id; | ||
@@ -53,2 +46,3 @@ this.client_secret = identity.secret; | ||
const response = await Axios.post(this.oauth_code_url, JSON.stringify(data), Constants.OAUTH.HEADERS).catch((error) => error); | ||
if (response instanceof Error) | ||
@@ -73,18 +67,2 @@ return this.emit('auth', { | ||
async getClientIdentity() { | ||
// The first request is made to get the auth script url, hard-coding it isn't viable as it changes overtime. | ||
const yttv_response = await Axios.get(`${Constants.URLS.YT_BASE_URL}/tv`, Constants.OAUTH.HEADERS).catch((error) => error); | ||
if (yttv_response instanceof Error) throw new Error(`Could not extract client identify: ${yttv_response.message}`); | ||
// Here we get the script and extract the necessary data to proceed with the auth flow. | ||
const url_body = this.auth_script_regex.exec(yttv_response.data)[1]; | ||
const script_url = `${Constants.URLS.YT_BASE_URL}/${url_body}`; | ||
const response = await Axios.get(script_url, Constants.DEFAULT_HEADERS).catch((error) => error); | ||
if (response instanceof Error) throw new Error(`Could not extract client identify: ${response.message}`); | ||
const identity_function = Utils.getStringBetweenStrings(response.data, 'setQuery("");', '{useGaiaSandbox:'); | ||
const client_identity = identity_function.replace(/\n/g, '').match(this.identity_regex); | ||
return client_identity.groups; | ||
} | ||
waitForAuth(device_code) { | ||
@@ -102,3 +80,3 @@ const data = { | ||
return this.emit('auth', { | ||
error: 'Could not get auth token.', | ||
error: 'Could not get authentication token.', | ||
status: 'FAILED' | ||
@@ -115,3 +93,3 @@ }); | ||
this.emit('auth', { | ||
error: 'The access was denied.', | ||
error: 'Access was denied.', | ||
status: 'ACCESS_DENIED' | ||
@@ -130,8 +108,11 @@ }); | ||
} else { | ||
const expiration_date = new Date(new Date().getTime() + response.data.expires_in * 1000); | ||
this.emit('auth', { | ||
access_token: response.data.access_token, | ||
refresh_token: response.data.refresh_token, | ||
credentials: { | ||
access_token: response.data.access_token, | ||
refresh_token: response.data.refresh_token, | ||
expires: expiration_date, | ||
}, | ||
token_type: response.data.token_type, | ||
expires: response.data.expires_in, | ||
scope: response.data.scope, | ||
status: 'SUCCESS' | ||
@@ -152,26 +133,55 @@ }); | ||
}; | ||
const response = await Axios.post(this.oauth_token_url, JSON.stringify(data), Constants.OAUTH.HEADERS).catch((error) => error); | ||
if (response instanceof Error) | ||
return this.emit('refresh-token', { | ||
if (response instanceof Error) { | ||
this.emit('auth', { | ||
error: 'Could not refresh access token.', | ||
status: 'FAILED' | ||
}); | ||
this.emit('refresh-token', { | ||
access_token: response.data.access_token, | ||
return { | ||
credentials: { | ||
access_token: this.auth_info.access_token, | ||
refresh_token: this.auth_info.refresh_token, | ||
expires: this.auth_info.expires | ||
}, | ||
status: 'FAILED' | ||
}; | ||
} | ||
const expiration_date = new Date(new Date().getTime() + response.data.expires_in * 1000); | ||
return { | ||
credentials: { | ||
refresh_token: refresh_token, | ||
access_token: response.data.access_token, | ||
expires: expiration_date | ||
}, | ||
token_type: response.data.token_type, | ||
expires: response.data.expires_in, | ||
scope: response.data.scope, | ||
status: 'SUCCESS' | ||
}); | ||
}; | ||
} | ||
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. | ||
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}`); | ||
async checkTokenValidity(access_token, session) { | ||
let headers = Constants.INNERTUBE_REQOPTS({ session }).headers; | ||
headers.authorization = `Bearer ${access_token}`; | ||
// Here we get the script and extract the necessary data to proceed with the auth flow. | ||
const url_body = this.auth_script_regex.exec(yttv_response.data)[1]; | ||
const script_url = `${Constants.URLS.YT_BASE_URL}/${url_body}`; | ||
const response = await Axios.post(this.guide_url, JSON.stringify({ context: session.context }), { headers }).catch((error) => error); | ||
if (response instanceof Error) return 'INVALID'; | ||
return 'VALID'; | ||
const response = await Axios.get(script_url, Constants.DEFAULT_HEADERS).catch((error) => error); | ||
if (response instanceof Error) throw new Error(`Could not extract client identity: ${response.message}`); | ||
const identity_function = Utils.getStringBetweenStrings(response.data, 'setQuery("");', '{useGaiaSandbox:'); | ||
const client_identity = identity_function.replace(/\n/g, '').match(this.identity_regex); | ||
return client_identity.groups; | ||
} | ||
@@ -178,0 +188,0 @@ } |
@@ -86,2 +86,21 @@ 'use strict'; | ||
module.exports = { getRandomUserAgent, generateSidAuth, getStringBetweenStrings, generateMessageParams, generateCommentParams, encodeNotificationPref }; | ||
function encodeFilter(period, duration, order) { | ||
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/proto/youtube.proto`)); | ||
const periods = { 'any': null, 'hour': 1, 'day': 2, 'week': 3, 'month': 4, 'year': 5 }; | ||
const durations = { 'any': null, 'short' : 1, 'long': 2 }; | ||
const orders = { 'relevance': null, 'rating': 1, 'age': 2, 'views' : 3 }; | ||
const search_filter_buff = youtube_proto.SearchFilter.encode({ | ||
number: orders[order], | ||
filter: { | ||
param_0: periods[period], | ||
param_1: (period == 'hour' && order == 'relevance') ? null : 1, | ||
param_2: durations[duration] | ||
} | ||
}); | ||
return encodeURIComponent(Buffer.from(search_filter_buff).toString('base64')); | ||
} | ||
module.exports = { getRandomUserAgent, generateSidAuth, getStringBetweenStrings, generateMessageParams, generateCommentParams, encodeNotificationPref, encodeFilter }; |
{ | ||
"name": "youtubei.js", | ||
"version": "1.2.7", | ||
"version": "1.2.8-npm", | ||
"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!", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -653,8 +653,7 @@ # YouTube.js | ||
// Only triggered when signing-in. | ||
youtube.on('auth', (data) => { | ||
youtube.ev.on('auth', (data) => { | ||
if (data.status === 'AUTHORIZATION_PENDING') { | ||
console.info(`Hello!\nOn your phone or computer, go to ${data.verification_url} and enter the code ${data.code}`); | ||
} else if (data.status === 'SUCCESS') { | ||
fs.writeFileSync(creds_path, JSON.stringify({ access_token: data.access_token, refresh_token: data.refresh_token })); | ||
fs.writeFileSync(creds_path, JSON.stringify(data.credentials)); | ||
console.info('Successfully signed-in, enjoy!'); | ||
@@ -664,5 +663,4 @@ } | ||
// Triggered whenever the access token is refreshed. | ||
youtube.on('update-credentials', (data) => { | ||
fs.writeFileSync(creds_path, JSON.stringify({ access_token: data.access_token, refresh_token: data.refresh_token })); | ||
youtube.ev.on('update-credentials', (data) => { | ||
fs.writeFileSync(creds_path, JSON.stringify(data.credentials)); | ||
console.info('Credentials updated!', data); | ||
@@ -669,0 +667,0 @@ }); |
@@ -13,23 +13,23 @@ 'use strict'; | ||
const youtube = await new Innertube().catch((error) => error); | ||
assert(youtube instanceof Error ? false : true, `should retrieve Innertube configuration data`); | ||
assert(youtube instanceof Error ? false : true, `should retrieve Innertube configuration data`, youtube); | ||
if (!(youtube instanceof Error)) { | ||
const search = await youtube.search('Carl Sagan - Documentary').catch((error) => error); | ||
assert((search instanceof Error ? false : true) && search.videos.length >= 1, `should search videos`); | ||
assert((search instanceof Error ? false : true) && 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}`); | ||
assert(details instanceof Error ? false : true, `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}`); | ||
assert(comments instanceof Error ? false : true, `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})`); | ||
assert(video instanceof Error ? false : true, `should download video (${Constants.test_video_id_1})`, video); | ||
} | ||
const n_token = new NToken(Constants.n_scramble_sc).transform(Constants.original_ntoken); | ||
assert(n_token == Constants.expected_ntoken, `should transform n token into ${Constants.expected_ntoken}`); | ||
assert(n_token == Constants.expected_ntoken, `should transform n token into ${Constants.expected_ntoken}`, n_token); | ||
const transformed_url = new SigDecipher(Constants.test_url, Constants.client_version, { sig_decipher_sc: Constants.sig_decipher_sc, ntoken_sc: Constants.n_scramble_sc }).decipher(); | ||
assert(transformed_url == Constants.expected_url, `should correctly decipher signature`); | ||
assert(transformed_url == Constants.expected_url, `should correctly decipher signature`, transformed_url); | ||
@@ -47,10 +47,10 @@ if (failed_tests > 0) | ||
stream.on('info', () => got_video_info = true); | ||
stream.on('error', () => reject()); | ||
stream.on('error', (err) => reject(err)); | ||
}); | ||
} | ||
function assert(outcome, description) { | ||
function assert(outcome, description, data) { | ||
const pass_fail = outcome ? 'pass' : 'fail'; | ||
!outcome && (failed_tests += 1); | ||
console.info(pass_fail, ':', description); | ||
console.info(pass_fail, ':', description, !outcome && `\nError: ${data}` || ''); | ||
return outcome; | ||
@@ -57,0 +57,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
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
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
95390
1355
1
703
1