diff --git a/lib/Actions.js b/lib/Actions.js index d569a928..4966e08c 100644 --- a/lib/Actions.js +++ b/lib/Actions.js @@ -4,16 +4,16 @@ const axios = require('axios'); const Utils = require('./Utils'); const Constants = require('./Constants'); -async function likeVideo (session, video_id) { +async function likeVideo(session, video_id) { if (!session.logged_in) throw new Error('You must be logged in to like a video'); let data = { context: session.context, - target: { + target: { videoId: video_id } }; - - const response = await axios.post(Constants.urls.YT_BASE_URL+'/youtubei/v1/like/like?key='+session.key, JSON.stringify(data), Constants.innertube_request_opts({ session, id: video_id, data })).catch((error) => error); + + const response = await axios.post(Constants.urls.YT_BASE_URL + '/youtubei/v1/like/like?key=' + session.key, JSON.stringify(data), Constants.innertube_request_opts({ session, id: video_id, data })).catch((error) => error); if (response instanceof Error) { return { success: false, @@ -29,7 +29,7 @@ async function likeVideo (session, video_id) { } } -async function removeLike (session, video_id) { +async function removeLike(session, video_id) { if (!session.logged_in) throw new Error('You must be logged in to remove a like/dislike.'); let data = { context: session.context, @@ -37,8 +37,8 @@ async function removeLike (session, video_id) { videoId: video_id } }; - - const response = await axios.post(Constants.urls.YT_BASE_URL+'/youtubei/v1/like/removelike?key='+session.key, JSON.stringify(data), Constants.innertube_request_opts({ session, id: video_id, data })).catch((error) => error); + + const response = await axios.post(Constants.urls.YT_BASE_URL + '/youtubei/v1/like/removelike?key=' + session.key, JSON.stringify(data), Constants.innertube_request_opts({ session, id: video_id, data })).catch((error) => error); if (response instanceof Error) { return { success: false, @@ -54,16 +54,16 @@ async function removeLike (session, video_id) { } } -async function dislikeVideo (session, video_id) { +async function dislikeVideo(session, video_id) { if (!session.logged_in) throw new Error('You must be logged in to like a video'); let data = { context: session.context, - target: { + target: { videoId: video_id } }; - - const response = await axios.post(Constants.urls.YT_BASE_URL+'/youtubei/v1/like/dislike?key='+session.key, JSON.stringify(data), Constants.innertube_request_opts({ session, id: video_id, data })).catch((error) => error); + + const response = await axios.post(Constants.urls.YT_BASE_URL + '/youtubei/v1/like/dislike?key=' + session.key, JSON.stringify(data), Constants.innertube_request_opts({ session, id: video_id, data })).catch((error) => error); if (response instanceof Error) { return { success: false, @@ -79,17 +79,17 @@ async function dislikeVideo (session, video_id) { } } -async function commentVideo (session, video_id, text) { +async function commentVideo(session, video_id, text) { if (!text) throw new Error('No text was provided'); if (!session.logged_in) throw new Error('You must be logged in to post a comment.'); - + let data = { context: session.context, commentText: text, createCommentParams: Utils.encodeId(video_id) }; - - const response = await axios.post(Constants.urls.YT_BASE_URL+'/youtubei/v1/comment/create_comment?key='+session.key, JSON.stringify(data), Constants.innertube_request_opts({ session, video_id, data })).catch((error) => error); + + const response = await axios.post(Constants.urls.YT_BASE_URL + '/youtubei/v1/comment/create_comment?key=' + session.key, JSON.stringify(data), Constants.innertube_request_opts({ session, video_id, data })).catch((error) => error); if (response instanceof Error) { return { success: false, diff --git a/lib/Constants.js b/lib/Constants.js index 56c3ed69..67b15f17 100644 --- a/lib/Constants.js +++ b/lib/Constants.js @@ -9,7 +9,7 @@ const urls = { }; const innertube_request_opts = (info) => { - if (info.desktop === undefined) info.desktop = true; + if (info.desktop === undefined) info.desktop = true; let req_opts = { headers: { 'accept': '*/*', @@ -25,20 +25,20 @@ const innertube_request_opts = (info) => { 'origin': info.desktop ? urls.YT_BASE_URL : urls.YT_MOBILE_URL, } }; - + if (info.session.logged_in && info.desktop) { req_opts.headers.Cookie = info.session.cookie; req_opts.headers.authorization = info.session.auth_apisid; } - + if (info.data) { - req_opts.headers['content-length'] = Buffer.byteLength(JSON.stringify(info.data), 'utf8'); + req_opts.headers['content-length'] = Buffer.byteLength(JSON.stringify(info.data), 'utf8'); } - + if (info.id) { - req_opts.headers.referer = (info.desktop ? urls.YT_BASE_URL : urls.YT_MOBILE_URL) +'/watch?v='+info.id; + req_opts.headers.referer = (info.desktop ? urls.YT_BASE_URL : urls.YT_MOBILE_URL) + '/watch?v=' + info.id; } - + return req_opts; }; @@ -47,7 +47,7 @@ const action_headers = info => { headers: { 'accept': '*/*', 'origin': urls.YT_BASE_URL, - 'referer': urls.YT_BASE_URL+'/watch?v='+info.id, + 'referer': urls.YT_BASE_URL + '/watch?v=' + info.id, 'user-agent': Utils.getRandomUserAgent('desktop').userAgent, 'content-type': 'application/json', 'content-length': Buffer.byteLength(JSON.stringify(info.data), 'utf8'), @@ -68,13 +68,13 @@ const search_headers = session => { const reqopts = { headers: { 'Cookie': session.cookie, - 'user-agent': Utils.getRandomUserAgent('desktop').userAgent, - 'content-type': 'application/json', - 'x-goog-authuser': '0', - 'x-goog-visitor-id': session.context.client.visitorData, - 'x-youtube-client-name': '1', - 'x-youtube-client-version': session.context.client.clientVersion, - 'x-origin': urls.YT_BASE_URL, + 'user-agent': Utils.getRandomUserAgent('desktop').userAgent, + 'content-type': 'application/json', + 'x-goog-authuser': '0', + 'x-goog-visitor-id': session.context.client.visitorData, + 'x-youtube-client-name': '1', + 'x-youtube-client-version': session.context.client.clientVersion, + 'x-origin': urls.YT_BASE_URL, } }; return reqopts; @@ -87,33 +87,33 @@ const videoinf_headers = (data, id, desktop) => { headers: { 'user-agent': Utils.getRandomUserAgent(desktop ? 'desktop' : 'mobile').userAgent, 'content-type': 'application/json', - 'x-goog-authuser': '0', - 'x-goog-visitor-id': data.context.client.visitorData, - 'x-youtube-client-name': '1', - 'x-youtube-client-version': data.context.client.clientVersion, - 'x-origin': 'https://www.youtube.com', - 'referer': 'https://www.youtube.com/watch?v='+id + 'x-goog-authuser': '0', + 'x-goog-visitor-id': data.context.client.visitorData, + 'x-youtube-client-name': '1', + 'x-youtube-client-version': data.context.client.clientVersion, + 'x-origin': 'https://www.youtube.com', + 'referer': 'https://www.youtube.com/watch?v=' + id } - }; + }; } else { req_opts = { headers: { 'accept': '*/*', 'user-agent': Utils.getRandomUserAgent(desktop ? 'desktop' : 'mobile').userAgent, - 'x-goog-visitor-id': data.context.client.visitorData, - 'sec-ch-ua': '"Google Chrome";v="93", " Not;A Brand";v="99", "Chromium";v="93"', - 'sec-ch-ua-platform-version': '10.0.0', - 'x-youtube-device': 'cbr=Chrome+Mobile&cbrand=Whatever&cbrver=93.0.4577.62&ceng=WebKit&cengver=537.36&cmodel=moto+g8+play&cos=Android&cosver=10&cplatform=MOBILE&cyear=2019', - 'x-youtube-client-name': '2', - 'x-youtube-chrome-connected': 'source=Chrome,mode=0,enable_account_consistency=true,supervised=false,consistency_enabled_by_default=false', - 'x-youtube-client-version': '2.20210908.00.00', - 'x-youtube-page-label': 'youtube.mobile.web.client_20210908_00_RC00', - 'x-spf-referer': 'https://m.youtube.com/watch?v='+id, - 'x-origin': 'https://m.youtube.com', - 'referer': 'https://m.youtube.com/watch?v='+id + 'x-goog-visitor-id': data.context.client.visitorData, + 'sec-ch-ua': '"Google Chrome";v="93", " Not;A Brand";v="99", "Chromium";v="93"', + 'sec-ch-ua-platform-version': '10.0.0', + 'x-youtube-device': 'cbr=Chrome+Mobile&cbrand=Whatever&cbrver=93.0.4577.62&ceng=WebKit&cengver=537.36&cmodel=moto+g8+play&cos=Android&cosver=10&cplatform=MOBILE&cyear=2019', + 'x-youtube-client-name': '2', + 'x-youtube-chrome-connected': 'source=Chrome,mode=0,enable_account_consistency=true,supervised=false,consistency_enabled_by_default=false', + 'x-youtube-client-version': '2.20210908.00.00', + 'x-youtube-page-label': 'youtube.mobile.web.client_20210908_00_RC00', + 'x-spf-referer': 'https://m.youtube.com/watch?v=' + id, + 'x-origin': 'https://m.youtube.com', + 'referer': 'https://m.youtube.com/watch?v=' + id } }; - } + } if (data.logged_in && desktop) { req_opts.headers.authorization = data.auth_apisid; req_opts.headers.cookie = data.cookie; @@ -137,37 +137,37 @@ const default_headers = (session) => { const video_data_req_body = (id, sts, context) => { return { - playbackContext : { + playbackContext: { contentPlaybackContext: { - 'currentUrl': '/watch?v='+id, + 'currentUrl': '/watch?v=' + id, 'vis': 0, 'splay': false, 'autoCaptionsDefaultOn': false, 'autonavState': 'STATE_OFF', - 'html5Preference': 'HTML5_PREF_WANTS', - 'signatureTimestamp': sts, - 'referer': urls.YT_BASE_URL, - 'lactMilliseconds': '-1' - } - }, - context : context, - videoId : id + 'html5Preference': 'HTML5_PREF_WANTS', + 'signatureTimestamp': sts, + 'referer': urls.YT_BASE_URL, + 'lactMilliseconds': '-1' + } + }, + context: context, + videoId: id }; }; const stream_headers = (range) => { - let headers = { - 'Accept': '*/*', - 'User-Agent': Utils.getRandomUserAgent('desktop').userAgent, - 'Connection': 'keep-alive', - 'Origin': urls.YT_BASE_URL, - 'Referer': urls.YT_BASE_URL, - 'DNT': '?1' - }; - if (range) { - headers.Range = range; - } - return headers; + let headers = { + 'Accept': '*/*', + 'User-Agent': Utils.getRandomUserAgent('desktop').userAgent, + 'Connection': 'keep-alive', + 'Origin': urls.YT_BASE_URL, + 'Referer': urls.YT_BASE_URL, + 'DNT': '?1' + }; + if (range) { + headers.Range = range; + } + return headers; }; const req_opts = { @@ -200,130 +200,130 @@ const filters = (order) => { return 'EgYIAhABGAE%3D'; case 'week,short,relevance': return "EgYIAxABGAE%3D"; - case "month,short,relevance": - return "EgYIBBABGAE%3D"; - case "year,short,relevance": - return "EgYIBRABGAE%3D"; - case "any,long,relevance": - return "EgQQARgC"; - case "hour,long,relevance": - return "EgYIARABGAI%3D"; - case "day,long,relevance": - return "EgYIAhABGAI%3D"; - case "week,long,relevance": - return "EgYIAxABGAI%3D"; - case "month,long,relevance": - return "EgYIBBABGAI%3D"; - case "year,long,relevance": - return "EgYIBRABGAI%3D"; - case "any,any,age": - return "CAISAhAB"; - case "hour,any,age": - return "CAISBAgBEAE%3D"; - case "day,any,age": - return "CAISBAgCEAE%3D"; - case "week,any,age": - return "CAISBAgDEAE%3D"; - case "month,any,age": - return "CAISBAgEEAE%3D"; - case "year,any,age": - return "CAISBAgFEAE%3D"; - case "any,short,age": - return "CAISBBABGAE%3D"; - case "hour,short,age": - return "CAISBggBEAEYAQ%3D%3D"; - case "day,short,age": - return "CAISBggCEAEYAQ%3D%3D"; - case "week,short,age": - return "CAISBggDEAEYAQ%3D%3D"; - case "month,short,age": - return "CAISBggEEAEYAQ%3D%3D"; - case "year,short,age": - return "CAISBggFEAEYAQ%3D%3D"; - case "any,long,age": - return "CAISBBABGAI%3D"; - case "hour,long,age": - return "CAISBggBEAEYAg%3D%3D"; - case "day,long,age": - return "CAISBggCEAEYAg%3D%3D"; - case "week,long,age": - return "CAISBggDEAEYAg%3D%3D"; - case "month,long,age": - return "CAISBggEEAEYAg%3D%3D"; - case"year,long,age": - return "CAISBggFEAEYAg%3D%3D"; - case "any,any,views": - return "CAMSAhAB"; - case "hour,any,views": - return "CAMSBAgBEAE%3D"; - case "day,any,views": - return "CAMSBAgCEAE%3D"; - case "week,any,views": - return "CAMSBAgDEAE%3D"; - case "month,any,views": - return "CAMSBAgEEAE%3D"; - case "year,any,views": - return "CAMSBAgFEAE%3D"; - case "any,short,views": - return "CAMSBBABGAE%3D"; - case "hour,short,views": - return "CAMSBggBEAEYAQ%3D%3D"; - case "day,short,views": - return "CAMSBggCEAEYAQ%3D%3D"; - case "week,short,views": - return "CAMSBggDEAEYAQ%3D%3D"; - case "month,short,views": - return "CAMSBggEEAEYAQ%3D%3D"; - case "year,short,views": - return "CAMSBggFEAEYAQ%3D%3D"; - case "any,long,views": - return "CAMSBBABGAI%3D"; - case "hour,long,views": - return "CAMSBggBEAEYAg%3D%3D"; - case "day,long,views": - return "CAMSBggCEAEYAg%3D%3D"; - case "week,long,views": - return "CAMSBggDEAEYAg%3D%3D"; - case "month,long,views": - return "CAMSBggEEAEYAg%3D%3D"; - case "year,long,views": - return "CAMSBggFEAEYAg%3D%3D"; - case "any,any,rating": - return "CAESAhAB"; - case "hour,any,rating": - return "CAESBAgBEAE%3D"; - case "day,any,rating": - return "CAESBAgCEAE%3D"; - case "week,any,rating": - return "CAESBAgDEAE%3D"; - case "month,any,rating": - return "CAESBAgEEAE%3D"; - case "year,any,rating": - return "CAESBAgFEAE%3D"; - case "any,short,rating": - return "CAESBBABGAE%3D"; - case "hour,short,rating": - return "CAESBggBEAEYAQ%3D%3D"; - case "day,short,rating": - return "CAESBggCEAEYAQ%3D%3D"; - case "week,short,rating": - return "CAESBggDEAEYAQ%3D%3D"; - case "month,short,rating": - return "CAESBggEEAEYAQ%3D%3D"; - case "year,short,rating": - return "CAESBggFEAEYAQ%3D%3D"; - case "any,long,rating": - return "CAESBBABGAI%3D"; - case "hour,long,rating": - return "CAESBggBEAEYAg%3D%3D"; - case "day,long,rating": - return "CAESBggCEAEYAg%3D%3D"; - case "week,long,rating": - return "CAESBggDEAEYAg%3D%3D"; - case "month,long,rating": - return "CAESBggEEAEYAg%3D%3D"; - case "year,long,rating": - return "CAESBggFEAEYAg%3D%3D"; + case "month,short,relevance": + return "EgYIBBABGAE%3D"; + case "year,short,relevance": + return "EgYIBRABGAE%3D"; + case "any,long,relevance": + return "EgQQARgC"; + case "hour,long,relevance": + return "EgYIARABGAI%3D"; + case "day,long,relevance": + return "EgYIAhABGAI%3D"; + case "week,long,relevance": + return "EgYIAxABGAI%3D"; + case "month,long,relevance": + return "EgYIBBABGAI%3D"; + case "year,long,relevance": + return "EgYIBRABGAI%3D"; + case "any,any,age": + return "CAISAhAB"; + case "hour,any,age": + return "CAISBAgBEAE%3D"; + case "day,any,age": + return "CAISBAgCEAE%3D"; + case "week,any,age": + return "CAISBAgDEAE%3D"; + case "month,any,age": + return "CAISBAgEEAE%3D"; + case "year,any,age": + return "CAISBAgFEAE%3D"; + case "any,short,age": + return "CAISBBABGAE%3D"; + case "hour,short,age": + return "CAISBggBEAEYAQ%3D%3D"; + case "day,short,age": + return "CAISBggCEAEYAQ%3D%3D"; + case "week,short,age": + return "CAISBggDEAEYAQ%3D%3D"; + case "month,short,age": + return "CAISBggEEAEYAQ%3D%3D"; + case "year,short,age": + return "CAISBggFEAEYAQ%3D%3D"; + case "any,long,age": + return "CAISBBABGAI%3D"; + case "hour,long,age": + return "CAISBggBEAEYAg%3D%3D"; + case "day,long,age": + return "CAISBggCEAEYAg%3D%3D"; + case "week,long,age": + return "CAISBggDEAEYAg%3D%3D"; + case "month,long,age": + return "CAISBggEEAEYAg%3D%3D"; + case "year,long,age": + return "CAISBggFEAEYAg%3D%3D"; + case "any,any,views": + return "CAMSAhAB"; + case "hour,any,views": + return "CAMSBAgBEAE%3D"; + case "day,any,views": + return "CAMSBAgCEAE%3D"; + case "week,any,views": + return "CAMSBAgDEAE%3D"; + case "month,any,views": + return "CAMSBAgEEAE%3D"; + case "year,any,views": + return "CAMSBAgFEAE%3D"; + case "any,short,views": + return "CAMSBBABGAE%3D"; + case "hour,short,views": + return "CAMSBggBEAEYAQ%3D%3D"; + case "day,short,views": + return "CAMSBggCEAEYAQ%3D%3D"; + case "week,short,views": + return "CAMSBggDEAEYAQ%3D%3D"; + case "month,short,views": + return "CAMSBggEEAEYAQ%3D%3D"; + case "year,short,views": + return "CAMSBggFEAEYAQ%3D%3D"; + case "any,long,views": + return "CAMSBBABGAI%3D"; + case "hour,long,views": + return "CAMSBggBEAEYAg%3D%3D"; + case "day,long,views": + return "CAMSBggCEAEYAg%3D%3D"; + case "week,long,views": + return "CAMSBggDEAEYAg%3D%3D"; + case "month,long,views": + return "CAMSBggEEAEYAg%3D%3D"; + case "year,long,views": + return "CAMSBggFEAEYAg%3D%3D"; + case "any,any,rating": + return "CAESAhAB"; + case "hour,any,rating": + return "CAESBAgBEAE%3D"; + case "day,any,rating": + return "CAESBAgCEAE%3D"; + case "week,any,rating": + return "CAESBAgDEAE%3D"; + case "month,any,rating": + return "CAESBAgEEAE%3D"; + case "year,any,rating": + return "CAESBAgFEAE%3D"; + case "any,short,rating": + return "CAESBBABGAE%3D"; + case "hour,short,rating": + return "CAESBggBEAEYAQ%3D%3D"; + case "day,short,rating": + return "CAESBggCEAEYAQ%3D%3D"; + case "week,short,rating": + return "CAESBggDEAEYAQ%3D%3D"; + case "month,short,rating": + return "CAESBggEEAEYAQ%3D%3D"; + case "year,short,rating": + return "CAESBggFEAEYAQ%3D%3D"; + case "any,long,rating": + return "CAESBBABGAI%3D"; + case "hour,long,rating": + return "CAESBggBEAEYAg%3D%3D"; + case "day,long,rating": + return "CAESBggCEAEYAg%3D%3D"; + case "week,long,rating": + return "CAESBggDEAEYAg%3D%3D"; + case "month,long,rating": + return "CAESBggEEAEYAg%3D%3D"; + case "year,long,rating": + return "CAESBggFEAEYAg%3D%3D"; default: } }; @@ -331,7 +331,7 @@ const filters = (order) => { const formatVideoData = (data, context, desktop) => { if (desktop) { let metadata = {}; - + metadata.embed = data.microformat.playerMicroformatRenderer.embed; metadata.view_count = parseInt(data.videoDetails.viewCount); metadata.average_rating = data.videoDetails.averageRating; @@ -349,7 +349,7 @@ const formatVideoData = (data, context, desktop) => { metadata.publish_date = data.microformat.playerMicroformatRenderer.publishDate; metadata.upload_date = data.microformat.playerMicroformatRenderer.uploadDate; metadata.keywords = data.videoDetails.keywords; - + let video_details = {}; video_details.title = data.videoDetails.title; video_details.description = data.videoDetails.shortDescription; @@ -358,10 +358,10 @@ const formatVideoData = (data, context, desktop) => { return video_details; } else { let metadata = {}; - + 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 = 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,'')); + 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 = 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, '')); 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; @@ -378,9 +378,9 @@ const formatVideoData = (data, context, desktop) => { 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; - + const id = data[2].playerResponse.videoDetails.videoId; - + let video_details = {}; video_details.title = data[2].playerResponse.videoDetails.title; video_details.description = data[2].playerResponse.videoDetails.shortDescription; diff --git a/lib/Innertube.js b/lib/Innertube.js index 49a9464f..be104a1b 100644 --- a/lib/Innertube.js +++ b/lib/Innertube.js @@ -13,116 +13,116 @@ class InnerTube { this.cookie = cookie || ''; return new Promise(async (resolve, reject) => resolve(await this.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 retrieve InnerTube configuration data: ' + response.message); - let innertube_data = JSON.parse('{'+Utils.getStringBetweenStrings(response.data, 'ytcfg.set({', '});')+'}'); + let innertube_data = JSON.parse('{' + Utils.getStringBetweenStrings(response.data, 'ytcfg.set({', '});') + '}'); if (innertube_data.INNERTUBE_CONTEXT) { this.context = innertube_data.INNERTUBE_CONTEXT; this.key = innertube_data.INNERTUBE_API_KEY; this.id_token = innertube_data.ID_TOKEN; this.session_token = innertube_data.XSRF_TOKEN; - this.player_url = innertube_data.PLAYER_JS_URL; + this.player_url = innertube_data.PLAYER_JS_URL; this.logged_in = innertube_data.LOGGED_IN; this.sts = innertube_data.STS; this.context.client.hl = 'en'; this.context.client.gl = 'US'; - + this.player = new Player(this); await this.player.init(); - + if (this.logged_in) { this.auth_apisid = Utils.getStringBetweenStrings(this.cookie, 'PAPISID=', ';'); - this.auth_apisid = Utils.generateSidAuth(this.auth_apisid); - } - + this.auth_apisid = Utils.generateSidAuth(this.auth_apisid); + } + this.initialized = true; } else { this.initialized = false; } return this; } - - async search (query, options = { period: 'any', order: 'relevance', duration: 'any', quantity: 100 }) { + + async search(query, options = { period: 'any', order: 'relevance', duration: 'any', quantity: 100 }) { if (!this.initialized) throw new Error('Missing Innertube data.'); - - const yt_response = await axios.post(Constants.urls.YT_BASE_URL+'/youtubei/v1/search?key='+this.key, JSON.stringify({ context: this.context, params: Constants.filters(options.period+','+options.duration+','+options.order), query }), Constants.innertube_request_opts({ session: this })).catch((error) => error); - if (yt_response instanceof Error) throw new Error('Could not search on YouTube: ' + yt_response.message); - - let content = yt_response.data.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents[0].itemSectionRenderer.contents; + + const yt_response = await axios.post(Constants.urls.YT_BASE_URL + '/youtubei/v1/search?key=' + this.key, JSON.stringify({ context: this.context, params: Constants.filters(options.period + ',' + options.duration + ',' + options.order), query }), Constants.innertube_request_opts({ session: this })).catch((error) => error); + if (yt_response instanceof Error) throw new Error('Could not search on YouTube: ' + yt_response.message); + + let content = yt_response.data.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents[0].itemSectionRenderer.contents; let search_response = {}; - + search_response.search_metadata = {}; search_response.search_metadata.query = content[0].showingResultsForRenderer ? content[0].showingResultsForRenderer.originalQuery.simpleText : query; search_response.search_metadata.corrected_query = content[0].showingResultsForRenderer ? content[0].showingResultsForRenderer.correctedQueryEndpoint.searchEndpoint.query : query; search_response.search_metadata.estimated_results = parseInt(yt_response.data.estimatedResults); search_response.videos = content.map((data) => { - if (!data.videoRenderer) return; - let video = data.videoRenderer; - return { - title: video.title.runs[0].text, - description: video.detailedMetadataSnippets ? video.detailedMetadataSnippets[0].snippetText.runs.map((item) => item.text).join('') : 'N/A', - 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, - durarion: { - 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_response; + if (!data.videoRenderer) return; + let video = data.videoRenderer; + return { + title: video.title.runs[0].text, + description: video.detailedMetadataSnippets ? video.detailedMetadataSnippets[0].snippetText.runs.map((item) => item.text).join('') : 'N/A', + 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, + durarion: { + 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_response; } - - download (id, options = {}) { + + download(id, options = {}) { options.quality = options.quality || '360p'; options.type = options.type || 'both'; options.format = options.format || 'mp4'; - + const stream = new Stream.PassThrough(); this.requestVideoInfo(id, true).then(async (video_data) => { let formats = []; - + if (video_data.playabilityStatus.status === 'LOGIN_REQUIRED') return stream.emit('error', { message: 'You must login to download age-restricted videos.', error_type: 'LOGIN_REQUIRED', playability_status: video_data.playabilityStatus.status }); if (!video_data.streamingData) return stream.emit('error', { message: 'Streaming data not available.', error_type: 'NO_STREAMING_DATA', playability_status: video_data.playabilityStatus.status }); formats = formats.concat(video_data.streamingData.formats || []).concat(video_data.streamingData.adaptiveFormats || []); formats.forEach((format) => { format.url = format.url || format.signatureCipher || format.cipher; - + if (format.signatureCipher || format.cipher) { format.url = new SigDecipher(format.url, this.context.client.clientVersion, this.player.dec_func, this.player.encodeN).decipher(); } - + format.has_audio = !!format.audioBitrate || !!format.audioQuality; format.has_video = !!format.qualityLabel; - + delete format.cipher; delete format.signatureCipher; }); - + formats.hls_manifest_url = video_data.streamingData.hlsManifestUrl || undefined; formats.dash_manifest_url = video_data.streamingData.dashManifestUrl || undefined; - + const video_details = Constants.formatVideoData(video_data, this, true); - + let url; let bitrates; let filtered_streams; - + switch (options.type) { case 'video': filtered_streams = formats.filter((format) => format.has_video && !format.has_audio); @@ -137,27 +137,27 @@ class InnerTube { filtered_streams = formats.filter((format) => format.has_video && format.has_audio); break; } - + if (options.type != 'both') { let streams; - + if (options.type != 'audio') { streams = filtered_streams.filter((format) => format.mimeType.includes(options.format || 'mp4') && format.qualityLabel == options.quality); } else { streams = filtered_streams.filter((format) => format.mimeType.includes(options.format || 'mp4')); } - + if (streams == undefined || streams.length == 0) { streams = filtered_streams.filter((format) => format.quality == 'medium'); } - + bitrates = streams.map((format) => format.bitrate); url = streams.filter((format) => format.bitrate === Math.max(...bitrates))[0]; } - + const selected_format = options.type == 'both' ? filtered_streams[0] : url; stream.emit('video_details', { video_details, selected_format, formats }); - + if (options.type == 'both') { const response = await axios.get(selected_format.url, { responseType: 'stream', headers: Constants.req_opts.stream_headers() }).catch((error) => error); if (response instanceof Error) { @@ -166,7 +166,7 @@ class InnerTube { } else { stream.emit('start'); } - + let downloaded_size = 0; response.data.on('data', (chunk) => { stream.write(new Buffer.from(chunk)); @@ -175,25 +175,25 @@ class InnerTube { let percentage = Math.floor((downloaded_size / response.headers['content-length']) * 100); stream.emit('progress', { chunk_size: chunk.length, downloaded_size: (downloaded_size / 1024 / 1024).toFixed(2), percentage, size }); }); - + response.data.on('error', (err) => stream.emit('error', { message: err.message, type: 'DOWNLOAD_ABORTED' })); response.data.on('end', () => stream.emit('end')); - } else { + } else { const chunk_size = 1048576 * 10; // 10MB - + let chunk_start = 0; let chunk_end = chunk_start + chunk_size; let downloaded_size = 0; - + stream.emit('start'); - + const downloadChunk = async () => { - + const response = await axios.get(selected_format.url, { responseType: 'stream', headers: Constants.req_opts.stream_headers(`bytes=${chunk_start}-${chunk_end || ''}`) }).catch((error) => error); if (response instanceof Error) { stream.emit('error', { message: response.message, type: 'REQUEST_FAILED' }); return stream; - } + } response.data.on('data', (chunk) => { stream.write(new Buffer.from(chunk)); @@ -202,27 +202,27 @@ class InnerTube { let percentage = Math.floor((downloaded_size / selected_format.contentLength) * 100); stream.emit('progress', { chunk_size: chunk.length, downloaded_size: (downloaded_size / 1024 / 1024).toFixed(2), percentage, size }); }); - + response.data.on('end', () => { chunk_start = chunk_end + 1; chunk_end += chunk_size; - + if (downloaded_size < selected_format.contentLength) { downloadChunk(); } else { setTimeout(() => stream.emit('end'), 100); // The delay ensures the last chunk is completely piped. - } + } }); response.data.on('error', (err) => stream.emit('error', { message: err.message, type: 'DOWNLOAD_ABORTED' })); }; downloadChunk(); - } + } }); - + return stream; } - - async getExtendedInfo (id) { + + async getExtendedInfo(id) { const data = await this.requestVideoInfo(id, false); const video_data = Constants.formatVideoData(data, this, false); @@ -230,18 +230,18 @@ class InnerTube { video_data.dislike = dislike => Actions.dislikeVideo(this, id); video_data.removeLike = remove_like => Actions.removeLike(this, id); video_data.comment = comment => Actions.commentVideo(this, id, comment); - + return video_data; } - - async requestVideoInfo (id, desktop) { + + async requestVideoInfo(id, desktop) { let response; if (!desktop) { - response = await axios.get(Constants.urls.YT_WATCH_PAGE+'?v='+id+'t=8s&pbj=1&bpctr=9999999999&has_verified=1&', Constants.innertube_request_opts({ session: this, id, desktop: false })).catch((error) => error); + response = await axios.get(Constants.urls.YT_WATCH_PAGE + '?v=' + id + 't=8s&pbj=1&bpctr=9999999999&has_verified=1&', Constants.innertube_request_opts({ session: this, id, desktop: false })).catch((error) => error); } else { - response = await axios.post(Constants.urls.YT_BASE_URL+'/youtubei/v1/player?key='+this.key, JSON.stringify(Constants.req_opts.video_data_req_body(id, this.sts, this.context)), Constants.innertube_request_opts({ session: this, id, desktop: true })).catch((error) => error); + response = await axios.post(Constants.urls.YT_BASE_URL + '/youtubei/v1/player?key=' + this.key, JSON.stringify(Constants.req_opts.video_data_req_body(id, this.sts, this.context)), Constants.innertube_request_opts({ session: this, id, desktop: true })).catch((error) => error); } - if (response instanceof Error) throw new Error('Could not retrieve watch page info: '+ response.message); + if (response instanceof Error) throw new Error('Could not retrieve watch page info: ' + response.message); return response.data; } } diff --git a/lib/Player.js b/lib/Player.js index 816c96fe..4b9a52e1 100644 --- a/lib/Player.js +++ b/lib/Player.js @@ -9,31 +9,31 @@ class Player { constructor(innertube_session) { this.session = innertube_session; this.player_name = Utils.getStringBetweenStrings(this.session.player_url, '/player/', '/'); - this.tmp_cache_dir = __dirname.slice(0, -3)+'cache'; + 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(); + 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); } else { - const response = await axios.get(Constants.urls.YT_BASE_URL+this.session.player_url, { path: this.session.playerUrl, headers: { 'content-type': 'text/javascript', 'user-agent': Utils.getRandomUserAgent('desktop').userAgent } }).catch((error) => error); - if (response instanceof Error) throw new Error ('Could not get player data: ' + response.message); + const response = await axios.get(Constants.urls.YT_BASE_URL + this.session.player_url, { path: this.session.playerUrl, headers: { 'content-type': 'text/javascript', 'user-agent': Utils.getRandomUserAgent('desktop').userAgent } }).catch((error) => error); + if (response instanceof Error) throw new Error('Could not get player data: ' + response.message); fs.mkdirSync(this.tmp_cache_dir, { recursive: true }); - fs.writeFileSync(this.tmp_cache_dir+'/'+this.player_name+'.js', response.data); + fs.writeFileSync(this.tmp_cache_dir + '/' + this.player_name + '.js', response.data); this.getSigDecipherCode(response.data); this.getNEncoder(response.data); } } - - getSigDecipherCode (data) { - const function_name = Utils.getStringBetweenStrings(data,'a.set("alr","yes");c&&(c=', '(decodeURIC'); + + getSigDecipherCode(data) { + const function_name = Utils.getStringBetweenStrings(data, 'a.set("alr","yes");c&&(c=', '(decodeURIC'); const func = Utils.getStringBetweenStrings(data, function_name + '=function(a)', '};') + '}'; this.dec_func = Utils.getStringBetweenStrings(data, 'var ' + Utils.getStringBetweenStrings(data, 'a=a.split("");', '.'), '};') + ';' + func + ';'; } - - getNEncoder (data) { + + getNEncoder(data) { const raw_code = 'var b=a.split("")' + Utils.getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}') + '} return b.join("");'; this.encodeN = Utils.createFunction('a', raw_code); } diff --git a/lib/SigDecipher.js b/lib/SigDecipher.js index cf26d2df..ddadff5f 100644 --- a/lib/SigDecipher.js +++ b/lib/SigDecipher.js @@ -3,7 +3,7 @@ const QueryString = require('querystring'); class SigDecipher { - constructor (url, cver, func_code, encode_n) { + constructor(url, cver, func_code, encode_n) { this.url = url; this.cver = cver; this.func_code = func_code; @@ -11,29 +11,29 @@ class SigDecipher { this.func_regex = /(.{2}):function\(.*?\){(.*?)}/g; this.actions_regex = /;.{2}\.(.{2})\(.*?,(.*?)\)/g; } - - decipher () { + + decipher() { const args = QueryString.parse(this.url); const functions = this.getFunctions(); - - function splice (arr, end) { + + function splice(arr, end) { arr.splice(0, end); } - - function swap (arr, position) { + + function swap(arr, position) { let origArrI = arr[0]; arr[0] = arr[position % arr.length]; arr[position % arr.length] = origArrI; } - - function reverse (options) { + + function reverse(options) { options.reverse(); } - + let actions; let signature = args.s.split(''); - - while((actions = this.actions_regex.exec(this.func_code)) !== null) { + + while ((actions = this.actions_regex.exec(this.func_code)) !== null) { switch (actions[1]) { case functions[0]: reverse(signature, actions[2]); @@ -47,7 +47,7 @@ class SigDecipher { default: } } - + const url_components = new URL(args.url); args.sp !== undefined ? url_components.searchParams.set(args.sp, signature.join('')) : url_components.searchParams.set('signature', signature.join('')); url_components.searchParams.set('cver', this.cver); @@ -57,12 +57,12 @@ class SigDecipher { url_components.searchParams.set('n', this.encode_n(url_components.searchParams.get('n'))); return url_components.toString(); } - - getFunctions () { + + getFunctions() { let func; let func_name = []; - - while((func = this.func_regex.exec(this.func_code)) !== null) { + + while ((func = this.func_regex.exec(this.func_code)) !== null) { if (func[0].includes('reverse()')) { func_name[0] = func[1]; } else if (func[0].includes('splice')) { @@ -71,7 +71,7 @@ class SigDecipher { func_name[2] = func[1]; } } - + return func_name; } } diff --git a/lib/Utils.js b/lib/Utils.js index 1cc74b8f..41a17756 100644 --- a/lib/Utils.js +++ b/lib/Utils.js @@ -3,23 +3,23 @@ const Crypto = require('crypto'); const UserAgent = require('user-agents'); -function encodeId (id) { - return encodeURI(new Buffer.from(` `+id+`*`).toString('base64').replace('==', '')+'BQBw=='); +function encodeId(id) { + return encodeURI(new Buffer.from(` ` + id + `*`).toString('base64').replace('==', '') + 'BQBw=='); } -function generateSidAuth (sid) { +function generateSidAuth(sid) { const youtube = 'https://www.youtube.com'; const timestamp = Math.floor(new Date().getTime() / 1000); const input = [timestamp, sid, youtube].join(' '); - + let hash = Crypto.createHash('sha1'); let data = hash.update(input, 'utf-8'); let gen_hash = data.digest('hex'); - + return ['SAPISIDHASH', [timestamp, gen_hash].join('_')].join(' '); } -function getRandomUserAgent (type) { +function getRandomUserAgent(type) { switch (type) { case 'mobile': return new UserAgent(/Android/).data; @@ -29,7 +29,7 @@ function getRandomUserAgent (type) { } } -function getStringBetweenStrings (data, start_string, end_string) { +function getStringBetweenStrings(data, start_string, end_string) { const regex = new RegExp(`${escapeStringRegexp(start_string)}(.*?)${escapeStringRegexp(end_string)}`, "s"); const match = data.match(regex); return match ? match[1] : undefined;