diff --git a/node-youtube-dl/YouTube/utils/cipher.ts b/node-youtube-dl/YouTube/utils/cipher.ts new file mode 100644 index 0000000..df2c1c5 --- /dev/null +++ b/node-youtube-dl/YouTube/utils/cipher.ts @@ -0,0 +1,173 @@ +import { URL } from 'node:url' +import { url_get } from './extractor' +import querystring from 'node:querystring' + +interface formatOptions { + url? : string; + sp? : string; + signatureCipher? : string; + cipher?: string; + s? : string; +} +const var_js = '[a-zA-Z_\\$][a-zA-Z_0-9]*'; +const singlequote_js = `'[^'\\\\]*(:?\\\\[\\s\\S][^'\\\\]*)*'`; +const duoblequote_js = `"[^"\\\\]*(:?\\\\[\\s\\S][^"\\\\]*)*"`; +const quote_js = `(?:${singlequote_js}|${duoblequote_js})`; +const key_js = `(?:${var_js}|${quote_js})`; +const prop_js = `(?:\\.${var_js}|\\[${quote_js}\\])`; +const empty_js = `(?:''|"")`; +const reverse_function = ':function\\(a\\)\\{' + +'(?:return )?a\\.reverse\\(\\)' + +'\\}'; +const slice_function = ':function\\(a,b\\)\\{' + +'return a\\.slice\\(b\\)' + +'\\}'; +const splice_function = ':function\\(a,b\\)\\{' + +'a\\.splice\\(0,b\\)' + +'\\}'; +const swap_function = ':function\\(a,b\\)\\{' + +'var c=a\\[0\\];a\\[0\\]=a\\[b(?:%a\\.length)?\\];a\\[b(?:%a\\.length)?\\]=c(?:;return a)?' + +'\\}'; +const obj_regexp = new RegExp( + `var (${var_js})=\\{((?:(?:${ + key_js}${reverse_function}|${ + key_js}${slice_function}|${ + key_js}${splice_function}|${ + key_js}${swap_function + }),?\\r?\\n?)+)\\};`) +const function_regexp = new RegExp(`${`function(?: ${var_js})?\\(a\\)\\{` + +`a=a\\.split\\(${empty_js}\\);\\s*` + +`((?:(?:a=)?${var_js}`}${ +prop_js +}\\(a,\\d+\\);)+)` + +`return a\\.join\\(${empty_js}\\)` + +`\\}`); +const reverse_regexp = new RegExp(`(?:^|,)(${key_js})${reverse_function}`, 'm'); +const slice_regexp = new RegExp(`(?:^|,)(${key_js})${slice_function}`, 'm'); +const splice_regexp = new RegExp(`(?:^|,)(${key_js})${splice_function}`, 'm'); +const swap_regexp = new RegExp(`(?:^|,)(${key_js})${swap_function}`, 'm'); + +export function js_tokens( body:string ) { + let function_action = function_regexp.exec(body) + let object_action = obj_regexp.exec(body) + if(!function_action || !object_action) return null + + let object = object_action[1].replace(/\$/g, '\\$') + let object_body = object_action[2].replace(/\$/g, '\\$') + let function_body = function_action[1].replace(/\$/g, '\\$') + + let result = reverse_regexp.exec(object_body); + const reverseKey = result && result[1] + .replace(/\$/g, '\\$') + .replace(/\$|^'|^"|'$|"$/g, ''); + + result = slice_regexp.exec(object_body) + const sliceKey = result && result[1] + .replace(/\$/g, '\\$') + .replace(/\$|^'|^"|'$|"$/g, ''); + + result = splice_regexp.exec(object_body); + const spliceKey = result && result[1] + .replace(/\$/g, '\\$') + .replace(/\$|^'|^"|'$|"$/g, ''); + + result = swap_regexp.exec(object_body); + const swapKey = result && result[1] + .replace(/\$/g, '\\$') + .replace(/\$|^'|^"|'$|"$/g, ''); + + const keys = `(${[reverseKey, sliceKey, spliceKey, swapKey].join('|')})`; + const myreg = `(?:a=)?${object + }(?:\\.${keys}|\\['${keys}'\\]|\\["${keys}"\\])` + + `\\(a,(\\d+)\\)`; + const tokenizeRegexp = new RegExp(myreg, 'g'); + const tokens = []; + while((result = tokenizeRegexp.exec(function_body)) !== null){ + let key = result[1] || result[2] || result[3]; + switch (key) { + case swapKey: + tokens.push(`sw${result[4]}`); + break; + case reverseKey: + tokens.push('rv'); + break; + case sliceKey: + tokens.push(`sl${result[4]}`); + break; + case spliceKey: + tokens.push(`sp${result[4]}`); + break; + } + } + return tokens +} + +export function deciper_signature(tokens : string[], signature :string){ + let sig = signature.split('') + let len = tokens.length + for(let i = 0; i < len; i++ ){ + let token = tokens[i], pos; + switch(token.slice(0,2)){ + case 'sw': + pos = parseInt(token.slice(2)) + sig = swappositions(sig, pos) + break + case 'rv': + sig = sig.reverse() + break + case 'sl': + pos = parseInt(token.slice(2)) + sig = sig.slice(pos) + break + case 'sp': + pos = parseInt(token.slice(2)) + sig.splice(0, pos) + break + } + } + return sig.join('') +} + + +function swappositions(array : string[], position : number){ + let first = array[0] + let pos_args = array[position] + array[0] = array[position] + array[position] = first + return array +} + +export function download_url(format: formatOptions, sig : string){ + let decoded_url; + if(!format.url) return; + decoded_url = format.url + + decoded_url = decodeURIComponent(decoded_url) + + let parsed_url = new URL(decoded_url) + parsed_url.searchParams.set('ratebypass', 'yes'); + + if(sig){ + parsed_url.searchParams.set(format.sp || 'signature', sig) + } + format.url = parsed_url.toString(); +} + +export async function format_decipher(format: formatOptions[], html5player : string){ + let body = await url_get(html5player) + let tokens = js_tokens(body) + format.forEach((format) => { + let cipher = format.signatureCipher || format.cipher; + if(cipher){ + Object.assign(format, querystring.parse(cipher)) + delete format.signatureCipher; + delete format.cipher; + } + let sig; + if(tokens && format.s){ + sig = deciper_signature(tokens, format.s) + download_url(format, sig) + } + }); + return format +} \ No newline at end of file diff --git a/node-youtube-dl/YouTube/utils/extractor.ts b/node-youtube-dl/YouTube/utils/extractor.ts index 461fcb5..c240591 100644 --- a/node-youtube-dl/YouTube/utils/extractor.ts +++ b/node-youtube-dl/YouTube/utils/extractor.ts @@ -1,6 +1,5 @@ import fetch from 'node-fetch' -import fs from 'fs' -import got from 'got' +import { format_decipher, js_tokens } from './cipher' export function valid_url(url : string): boolean{ let valid_url = /^https?:\/\/(youtu\.be\/|(www\.)?youtube\.com\/(embed|watch|v|shorts)(\/|\?))/ @@ -8,30 +7,59 @@ export function valid_url(url : string): boolean{ else return false } -export async function getBasicInfo(url : string){ +export async function yt_initial_data(url : string){ if(valid_url(url)){ let body = await url_get(url) - let final = { - player_response : get_ytPlayerResponse(body), - response : get_ytInitialData(body), - js_url : js_url(body) + let player_response = JSON.parse(body.split("var ytInitialPlayerResponse = ")[1].split(";")[0]) + let response = JSON.parse(body.split("var ytInitialData = ")[1].split(";")[0]) + let html5player = 'https://www.youtube.com' + body.split('"jsUrl":"')[1].split('"')[0] + let format = [] + format.push(player_response.streamingData.formats[0]) + format.push(...player_response.streamingData.adaptiveFormats) + let vid = player_response.videoDetails + let microformat = player_response.microformat.playerMicroformatRenderer + let video_details = { + id : vid.videoId, + url : 'https://www.youtube.com/watch?v=' + vid.videoId, + title : vid.title, + description : vid.shortDescription, + duration : vid.lengthSeconds, + uploadedDate : microformat.publishDate, + thumbnail : `https://i.ytimg.com/vi/${vid.videoId}/maxresdefault.jpg`, + channel : { + name : vid.author, + id : vid.channelId, + url : `https://www.youtube.com/channel/${vid.channelId}` + }, + views : vid.viewCount, + tags : vid.keywords, + averageRating : vid.averageRating, + live : vid.isLiveContent, + private : vid.isPrivate } + let final = { + player_response, + response, + html5player, + format, + video_details + } + return final } else { throw 'Not a Valid YouTube URL' } } -function js_url(data:string): string { - return data.split('"jsUrl":"')[1].split('"')[0] -} - -function get_ytPlayerResponse(data : string): JSON { - return JSON.parse(data.split("var ytInitialPlayerResponse = ")[1].split(";")[0]) -} - -function get_ytInitialData(data:string): JSON { - return JSON.parse(data.split("var ytInitialData = ")[1].split(";")[0]) +export async function yt_deciphered_data(url : string) { + let data = await yt_initial_data(url) + if(data.format[0].signatureCipher || data.format[0].cipher){ + data.format = await format_decipher(data.format, data.html5player) + return data + } + else { + return data + } } export async function url_get (url : string) : Promise{ diff --git a/node-youtube-dl/index.ts b/node-youtube-dl/index.ts index d593d68..6ac1355 100644 --- a/node-youtube-dl/index.ts +++ b/node-youtube-dl/index.ts @@ -1,3 +1,10 @@ -import { getBasicInfo } from "./YouTube/utils/extractor"; +import { yt_deciphered_data } from "./YouTube/utils/extractor"; -getBasicInfo('https://www.youtube.com/watch?v=IFYJNLZT_B0') \ No newline at end of file +let main = async() => { + let time_start = Date.now() + await yt_deciphered_data('https://www.youtube.com/watch?v=jbMHA3P7RzU') + let time_end = Date.now() + console.log(`Time Taken : ${(time_end - time_start)/1000} seconds`) +} + +main() \ No newline at end of file