Comparing version 0.0.29 to 0.0.30
@@ -9,4 +9,5 @@ import { Channel } from "../interfaces/Channel"; | ||
declare function getNumberFromText(str: string): number; | ||
declare function replaceAll(search: string, value: string, source: string): string; | ||
declare function processRendererItems(arr: Array<any>, httpclient: WrappedHTTPClient): (Video | Channel | Playlist | Comment | CommentThread | undefined)[]; | ||
export declare function getVideoDefaultThumbnail(videoId: string): { | ||
declare function getVideoDefaultThumbnail(videoId: string): { | ||
url: string; | ||
@@ -16,2 +17,4 @@ height: number; | ||
}; | ||
declare function getIndexBefore(str: string, index: number, source: string): number; | ||
declare function getIndexAfter(str: string, index: number, source: string): number; | ||
declare const _default: { | ||
@@ -23,3 +26,6 @@ recursiveSearchForPair: typeof recursiveSearchForPair; | ||
getVideoDefaultThumbnail: typeof getVideoDefaultThumbnail; | ||
replaceAll: typeof replaceAll; | ||
getIndexAfter: typeof getIndexAfter; | ||
getIndexBefore: typeof getIndexBefore; | ||
}; | ||
export default _default; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getVideoDefaultThumbnail = void 0; | ||
const Channel_1 = require("../interfaces/Channel"); | ||
@@ -51,7 +50,4 @@ const Playlist_1 = require("../interfaces/Playlist"); | ||
} | ||
function replaceAll(regex, value, source) { | ||
while (source.includes(regex)) { | ||
source = source.replace(regex, value); | ||
} | ||
return source; | ||
function replaceAll(search, value, source) { | ||
return source.replace(new RegExp(search, 'g'), value); | ||
} | ||
@@ -123,9 +119,19 @@ function processRendererItems(arr, httpclient) { | ||
} | ||
exports.getVideoDefaultThumbnail = getVideoDefaultThumbnail; | ||
function getIndexBefore(str, index, source) { | ||
var before = source.substring(0, index); | ||
return before.lastIndexOf(str); | ||
} | ||
function getIndexAfter(str, index, source) { | ||
var after = source.substring(index, source.length); | ||
return index + after.indexOf(str); | ||
} | ||
exports.default = { | ||
recursiveSearchForPair: recursiveSearchForPair, | ||
recursiveSearchForKey: recursiveSearchForKey, | ||
getNumberFromText: getNumberFromText, | ||
processRendererItems: processRendererItems, | ||
getVideoDefaultThumbnail: getVideoDefaultThumbnail, | ||
recursiveSearchForPair, | ||
recursiveSearchForKey, | ||
getNumberFromText, | ||
processRendererItems, | ||
getVideoDefaultThumbnail, | ||
replaceAll, | ||
getIndexAfter, | ||
getIndexBefore | ||
}; |
@@ -1,11 +0,2 @@ | ||
import { HTTPClient } from './main'; | ||
export declare class CiphService { | ||
httpClient: HTTPClient; | ||
constructor(httpClient: HTTPClient); | ||
cache: Map<any, any>; | ||
getTokens(html5playerfile: any, options: any): Promise<string[]>; | ||
extractActions(body: any): string[] | null; | ||
decipherFormats(formats: any, html5player: any, options: any): Promise<any>; | ||
decipher: (tokens: any, sig: any) => any; | ||
setDownloadURL(format: any, sig: any): void; | ||
} | ||
import { WrappedHTTPClient } from "./WrappedHTTPClient"; | ||
export declare function decipher(formats: Array<any>, playerURL: string, httpClient: WrappedHTTPClient): Promise<any[]>; |
@@ -12,180 +12,65 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.CiphService = void 0; | ||
const url = require("url"); | ||
const querystring = require("querystring"); | ||
exports.decipher = void 0; | ||
const helpers_1 = require("./fetchers/helpers"); | ||
const HTTPClient_1 = require("./interfaces/HTTPClient"); | ||
const jsVarStr = '[a-zA-Z_\\$][a-zA-Z_0-9]*'; | ||
const jsSingleQuoteStr = `'[^'\\\\]*(:?\\\\[\\s\\S][^'\\\\]*)*'`; | ||
const jsDoubleQuoteStr = `"[^"\\\\]*(:?\\\\[\\s\\S][^"\\\\]*)*"`; | ||
const jsQuoteStr = `(?:${jsSingleQuoteStr}|${jsDoubleQuoteStr})`; | ||
const jsKeyStr = `(?:${jsVarStr}|${jsQuoteStr})`; | ||
const jsPropStr = `(?:\\.${jsVarStr}|\\[${jsQuoteStr}\\])`; | ||
const jsEmptyStr = `(?:''|"")`; | ||
const reverseStr = ':function\\(a\\)\\{' + | ||
'(?:return )?a\\.reverse\\(\\)' + | ||
'\\}'; | ||
const sliceStr = ':function\\(a,b\\)\\{' + | ||
'return a\\.slice\\(b\\)' + | ||
'\\}'; | ||
const spliceStr = ':function\\(a,b\\)\\{' + | ||
'a\\.splice\\(0,b\\)' + | ||
'\\}'; | ||
const swapStr = ':function\\(a,b\\)\\{' + | ||
'var c=a\\[0\\];a\\[0\\]=a\\[b(?:%a\\.length)?\\];a\\[b(?:%a\\.length)?\\]=c(?:;return a)?' + | ||
'\\}'; | ||
const actionsObjRegexp = new RegExp(`var (${jsVarStr})=\\{((?:(?:` + | ||
jsKeyStr + reverseStr + '|' + | ||
jsKeyStr + sliceStr + '|' + | ||
jsKeyStr + spliceStr + '|' + | ||
jsKeyStr + swapStr + | ||
'),?\\r?\\n?)+)\\};'); | ||
const actionsFuncRegexp = new RegExp(`function(?: ${jsVarStr})?\\(a\\)\\{` + | ||
`a=a\\.split\\(${jsEmptyStr}\\);\\s*` + | ||
`((?:(?:a=)?${jsVarStr}` + | ||
jsPropStr + | ||
'\\(a,\\d+\\);)+)' + | ||
`return a\\.join\\(${jsEmptyStr}\\)` + | ||
'\\}'); | ||
const reverseRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${reverseStr}`, 'm'); | ||
const sliceRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${sliceStr}`, 'm'); | ||
const spliceRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${spliceStr}`, 'm'); | ||
const swapRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${swapStr}`, 'm'); | ||
class CiphService { | ||
constructor(httpClient) { | ||
this.httpClient = httpClient; | ||
this.cache = new Map(); | ||
this.decipher = (tokens, sig) => { | ||
sig = sig.split(''); | ||
for (let i = 0, len = tokens.length; i < len; i++) { | ||
let token = tokens[i], pos; | ||
switch (token[0]) { | ||
case 'r': | ||
sig = sig.reverse(); | ||
break; | ||
case 'w': | ||
pos = ~~token.slice(1); | ||
const first = sig[0]; | ||
sig[0] = sig[pos % sig.length]; | ||
sig[pos] = first; | ||
break; | ||
case 's': | ||
pos = ~~token.slice(1); | ||
sig = sig.slice(pos); | ||
break; | ||
case 'p': | ||
pos = ~~token.slice(1); | ||
sig.splice(0, pos); | ||
break; | ||
} | ||
const REGEXES = [ | ||
new RegExp("(?:\\b|[^a-zA-Z0-9$])([a-zA-Z0-9$]{2,})\\s*=\\s*function\\(\\s*a\\s*\\)" + "\\s*\\{\\s*a\\s*=\\s*a\\.split\\(\\s*\"\"\\s*\\)"), | ||
new RegExp("\\bm=([a-zA-Z0-9$]{2,})\\(decodeURIComponent\\(h\\.s\\)\\)"), | ||
new RegExp("\\bc&&\\(c=([a-zA-Z0-9$]{2,})\\(decodeURIComponent\\(c\\)\\)"), | ||
new RegExp("([\\w$]+)\\s*=\\s*function\\((\\w+)\\)\\{\\s*\\2=\\s*\\2\\.split\\(\"\"\\)\\s*;"), | ||
new RegExp("\\b([\\w$]{2,})\\s*=\\s*function\\((\\w+)\\)\\{\\s*\\2=\\s*\\2\\.split\\(\"\"\\)\\s*;"), | ||
new RegExp("\\bc\\s*&&\\s*d\\.set\\([^,]+\\s*,\\s*(:encodeURIComponent\\s*\\()([a-zA-Z0-9$]+)\\(") | ||
]; | ||
function decipher(formats, playerURL, httpClient) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const playerResults = yield httpClient.request({ | ||
method: HTTPClient_1.HTTPRequestMethod.GET, | ||
url: playerURL | ||
}); | ||
const playerJS = playerResults.data; | ||
let deobfuscateFunctionName = ""; | ||
for (const reg of REGEXES) { | ||
deobfuscateFunctionName = matchGroup1(reg, playerJS); | ||
if (deobfuscateFunctionName) { | ||
break; | ||
} | ||
return sig.join(''); | ||
}; | ||
} | ||
getTokens(html5playerfile, options) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const cachedTokens = this.cache.get(html5playerfile); | ||
const response = yield this.httpClient.request({ | ||
method: HTTPClient_1.HTTPRequestMethod.GET, | ||
url: html5playerfile | ||
}); | ||
const tokens = this.extractActions(response.data); | ||
if (!tokens || !tokens.length) { | ||
throw new Error('Could not extract signature deciphering actions'); | ||
} | ||
this.cache.set(html5playerfile, tokens); | ||
return tokens; | ||
}); | ||
} | ||
extractActions(body) { | ||
const objResult = actionsObjRegexp.exec(body); | ||
const funcResult = actionsFuncRegexp.exec(body); | ||
if (!objResult || !funcResult) { | ||
return null; | ||
} | ||
const obj = objResult[1].replace(/\$/g, '\\$'); | ||
const objBody = objResult[2].replace(/\$/g, '\\$'); | ||
const funcBody = funcResult[1].replace(/\$/g, '\\$'); | ||
let result = reverseRegexp.exec(objBody); | ||
const reverseKey = result && result[1] | ||
.replace(/\$/g, '\\$') | ||
.replace(/\$|^'|^"|'$|"$/g, ''); | ||
result = sliceRegexp.exec(objBody); | ||
const sliceKey = result && result[1] | ||
.replace(/\$/g, '\\$') | ||
.replace(/\$|^'|^"|'$|"$/g, ''); | ||
result = spliceRegexp.exec(objBody); | ||
const spliceKey = result && result[1] | ||
.replace(/\$/g, '\\$') | ||
.replace(/\$|^'|^"|'$|"$/g, ''); | ||
result = swapRegexp.exec(objBody); | ||
const swapKey = result && result[1] | ||
.replace(/\$/g, '\\$') | ||
.replace(/\$|^'|^"|'$|"$/g, ''); | ||
const keys = `(${[reverseKey, sliceKey, spliceKey, swapKey].join('|')})`; | ||
const myreg = '(?:a=)?' + obj + | ||
`(?:\\.${keys}|\\['${keys}'\\]|\\["${keys}"\\])` + | ||
'\\(a,(\\d+)\\)'; | ||
const tokenizeRegexp = new RegExp(myreg, 'g'); | ||
const tokens = []; | ||
while ((result = tokenizeRegexp.exec(funcBody)) !== null) { | ||
const key = result[1] || result[2] || result[3]; | ||
switch (key) { | ||
case swapKey: | ||
tokens.push('w' + result[4]); | ||
break; | ||
case reverseKey: | ||
tokens.push('r'); | ||
break; | ||
case sliceKey: | ||
tokens.push('s' + result[4]); | ||
break; | ||
case spliceKey: | ||
tokens.push('p' + result[4]); | ||
break; | ||
const functionPattern = new RegExp("(" + deobfuscateFunctionName.replace("$", "\\$") + "=function\\([a-zA-Z0-9_]+\\)\\{.+?\\})"); | ||
const deobfuscateFunction = "var " + matchGroup1(functionPattern, playerJS) + ";"; | ||
const helperObjectName = matchGroup1(new RegExp(";([A-Za-z0-9_\\$]{2})\\...\\("), deobfuscateFunction); | ||
const helperPattern = new RegExp("(var " + helperObjectName + "=\\{.+?\\}\\};)"); | ||
const helperObject = matchGroup1(helperPattern, helpers_1.default.replaceAll("\n", "", playerJS)); | ||
const finalFunc = eval(`(function getDecipherFunction() { | ||
` + helperObject + ` | ||
` + deobfuscateFunction + ` | ||
return (val) => ` + deobfuscateFunctionName + `; | ||
})()`)(); | ||
for (const format of formats) { | ||
if (format.signatureCipher) { | ||
const signatureParams = parseQuery(format.signatureCipher); | ||
const resolvedSignature = finalFunc(signatureParams.s); | ||
const finalURL = new URL(signatureParams.url); | ||
finalURL.searchParams.set(signatureParams.sp, resolvedSignature); | ||
format.url = finalURL.toString(); | ||
} | ||
} | ||
return tokens; | ||
return formats; | ||
}); | ||
} | ||
exports.decipher = decipher; | ||
function matchGroup1(regex, str) { | ||
const res = regex.exec(str); | ||
if (!res) | ||
return ""; | ||
return res[1]; | ||
} | ||
function parseQuery(queryString) { | ||
var query = {}; | ||
var pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&'); | ||
for (var i = 0; i < pairs.length; i++) { | ||
var pair = pairs[i].split('='); | ||
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || ''); | ||
} | ||
decipherFormats(formats, html5player, options) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const decipheredFormats = []; | ||
const tokens = yield this.getTokens(html5player, options); | ||
formats.forEach((format) => { | ||
const cipher = format.signatureCipher || format.cipher; | ||
if (cipher) { | ||
Object.assign(format, querystring.parse(cipher)); | ||
delete format.signatureCipher; | ||
delete format.cipher; | ||
} | ||
const sig = tokens && format.s ? this.decipher(tokens, format.s) : null; | ||
this.setDownloadURL(format, sig); | ||
decipheredFormats.push(format); | ||
}); | ||
return decipheredFormats; | ||
}); | ||
} | ||
setDownloadURL(format, sig) { | ||
let decodedUrl; | ||
if (format.url) { | ||
decodedUrl = format.url; | ||
} | ||
else { | ||
return; | ||
} | ||
try { | ||
decodedUrl = decodeURIComponent(decodedUrl); | ||
} | ||
catch (err) { | ||
return; | ||
} | ||
const parsedUrl = url.parse(decodedUrl, true); | ||
delete parsedUrl.search; | ||
const query = parsedUrl.query; | ||
query.ratebypass = 'yes'; | ||
if (sig) { | ||
query[format.sp || 'signature'] = sig; | ||
} | ||
format.url = url.format(parsedUrl); | ||
} | ||
return query; | ||
} | ||
exports.CiphService = CiphService; |
@@ -17,3 +17,3 @@ "use strict"; | ||
const HTTPClient_1 = require("./HTTPClient"); | ||
const formatsChipher_1 = require("../formatsChipher"); | ||
const formatsChipher = require("../formatsChipher"); | ||
class Video { | ||
@@ -183,2 +183,3 @@ constructor(httpclient) { | ||
} | ||
yield this.loadFormats(); | ||
}); | ||
@@ -188,38 +189,23 @@ } | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const playerResponse = yield this.httpclient.request({ | ||
method: HTTPClient_1.HTTPRequestMethod.POST, | ||
url: constants_1.ENDPOINT_PLAYER, | ||
data: { | ||
videoId: this.videoId, | ||
racyCheckOk: false, | ||
contentCheckOk: false, | ||
playbackContext: { | ||
contentPlaybackContent: { | ||
currentUrl: "/watch?v=6Dh-RL__uN4", | ||
autonavState: "STATE_OFF", | ||
autoCaptionsDefaultOn: false, | ||
html5Preference: "HTML5_PREF_WANTS", | ||
lactMilliseconds: "-1", | ||
referer: "https://www.youtube.com/", | ||
signatureTimestamp: 19095, | ||
splay: false, | ||
vis: 0 | ||
} | ||
} | ||
} | ||
}); | ||
const playerJSON = yield JSON.parse(playerResponse.data); | ||
const formats = helpers_1.default.recursiveSearchForKey("adaptiveFormats", playerJSON)[0]; | ||
const watchURL = new URL(constants_1.ENDPOINT_WATCHPAGE); | ||
watchURL.searchParams.set("v", this.videoId); | ||
const playPage = yield this.httpclient.request({ | ||
const playPage = yield this.httpclient.client.request({ | ||
method: HTTPClient_1.HTTPRequestMethod.GET, | ||
url: watchURL.toString() | ||
url: watchURL.toString(), | ||
headers: { | ||
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" | ||
} | ||
}); | ||
const html = playPage.data; | ||
const varFind = html.indexOf("var ytInitialPlayerResponse ="); | ||
const scriptStart = helpers_1.default.getIndexAfter(">", helpers_1.default.getIndexBefore("<script", varFind, html), html) + 1; | ||
const scriptEnd = helpers_1.default.getIndexAfter("</script>", varFind, html); | ||
const script = html.substring(scriptStart, scriptEnd); | ||
const scriptFunc = new Function(script + " return ytInitialPlayerResponse;"); | ||
const playerJSON = scriptFunc(); | ||
const formats = helpers_1.default.recursiveSearchForKey("adaptiveFormats", playerJSON)[0]; | ||
let playerScript = /<script\s+src="([^"]+)"(?:\s+type="text\/javascript")?\s+name="player_ias\/base"\s*>|"jsUrl":"([^"]+)"/.exec(html); | ||
playerScript = playerScript[2] || playerScript[1]; | ||
const playerURLString = new URL(playerScript, watchURL).href; | ||
const cipher = new formatsChipher_1.CiphService(this.httpclient); | ||
const resolvedFormats = yield cipher.decipherFormats(formats, playerURLString, {}); | ||
const resolvedFormats = yield formatsChipher.decipher(formats, playerURLString, this.httpclient); | ||
this.formats = []; | ||
@@ -226,0 +212,0 @@ resolvedFormats.forEach((a) => { |
@@ -28,3 +28,4 @@ import { Authenticator } from "./Authenticator"; | ||
getUser(): User; | ||
getCookies(): string; | ||
throwErrorIfNotReady(): void; | ||
} |
@@ -112,2 +112,5 @@ "use strict"; | ||
} | ||
getCookies() { | ||
return this.wrappedHttpClient.cookieString; | ||
} | ||
throwErrorIfNotReady() { | ||
@@ -114,0 +117,0 @@ if (!this.storageAdapter) |
@@ -22,2 +22,3 @@ import { Explorer } from "./fetchers/Explorer"; | ||
import { default as IYoutube } from "./IYoutube"; | ||
import { HTTPRequestOptions, HTTPRequestMethod, HTTPResponse } from "./interfaces/HTTPClient"; | ||
export { IYoutube as IYoutube }; | ||
@@ -44,2 +45,5 @@ export { HTTPClient as HTTPClient }; | ||
export { Format as Format }; | ||
export { HTTPRequestMethod as HTTPRequestMethod }; | ||
export { HTTPRequestOptions as HTTPRequestOptions }; | ||
export { HTTPResponse as HTTPResponse }; | ||
export declare const nodeDefault: () => Promise<IYoutube>; |
@@ -12,3 +12,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.nodeDefault = exports.List = exports.CommentThreadRepliesContinuatedList = exports.CommentThread = exports.Comment = exports.CommentSectionContinuatedList = exports.Playlist = exports.Video = exports.Channel = exports.Authenticator = exports.ContinuatedList = exports.User = exports.Explorer = exports.WrappedHTTPClient = exports.IYoutube = void 0; | ||
exports.nodeDefault = exports.HTTPRequestMethod = exports.List = exports.CommentThreadRepliesContinuatedList = exports.CommentThread = exports.Comment = exports.CommentSectionContinuatedList = exports.Playlist = exports.Video = exports.Channel = exports.Authenticator = exports.ContinuatedList = exports.User = exports.Explorer = exports.WrappedHTTPClient = exports.IYoutube = void 0; | ||
const Explorer_1 = require("./fetchers/Explorer"); | ||
@@ -42,2 +42,4 @@ Object.defineProperty(exports, "Explorer", { enumerable: true, get: function () { return Explorer_1.Explorer; } }); | ||
Object.defineProperty(exports, "IYoutube", { enumerable: true, get: function () { return IYoutube_1.default; } }); | ||
const HTTPClient_1 = require("./interfaces/HTTPClient"); | ||
Object.defineProperty(exports, "HTTPRequestMethod", { enumerable: true, get: function () { return HTTPClient_1.HTTPRequestMethod; } }); | ||
const nodeDefault = () => __awaiter(void 0, void 0, void 0, function* () { | ||
@@ -44,0 +46,0 @@ const path = "./nodeDefault"; |
{ | ||
"name": "iyoutube", | ||
"version": "0.0.29", | ||
"version": "0.0.30", | ||
"description": "The ultimate unofficial YouTube API Client for Javascript", | ||
@@ -5,0 +5,0 @@ "main": "output/main.js", |
@@ -53,7 +53,4 @@ import { Channel } from "../interfaces/Channel"; | ||
function replaceAll(regex:string, value: string, source:string) { | ||
while(source.includes(regex)) { | ||
source = source.replace(regex, value); | ||
} | ||
return source; | ||
function replaceAll(search:string, value: string, source:string) { | ||
return source.replace(new RegExp(search, 'g'), value); | ||
} | ||
@@ -128,3 +125,3 @@ | ||
export function getVideoDefaultThumbnail(videoId:string) { | ||
function getVideoDefaultThumbnail(videoId:string) { | ||
return { | ||
@@ -136,8 +133,21 @@ url: "https://i.ytimg.com/vi/" + videoId + "/maxresdefault.jpg", | ||
} | ||
function getIndexBefore(str: string, index: number, source: string) { | ||
var before = source.substring(0, index); | ||
return before.lastIndexOf(str); | ||
} | ||
function getIndexAfter(str: string, index: number, source: string) { | ||
var after = source.substring(index, source.length); | ||
return index + after.indexOf(str); | ||
} | ||
export default { | ||
recursiveSearchForPair: recursiveSearchForPair, | ||
recursiveSearchForKey: recursiveSearchForKey, | ||
getNumberFromText: getNumberFromText, | ||
processRendererItems: processRendererItems, | ||
getVideoDefaultThumbnail: getVideoDefaultThumbnail, | ||
recursiveSearchForPair, | ||
recursiveSearchForKey, | ||
getNumberFromText, | ||
processRendererItems, | ||
getVideoDefaultThumbnail, | ||
replaceAll, | ||
getIndexAfter, | ||
getIndexBefore | ||
} |
@@ -1,210 +0,73 @@ | ||
//From https://github.com/appit-online/ionic-youtube-streams/blob/eeee1741857f06c81380c317bd6e71732c895ee1/src/lib/cip.service.ts#L53 | ||
import * as url from 'url'; | ||
import * as querystring from 'querystring'; | ||
import { HTTPClient } from './main'; | ||
import { HTTPRequestMethod } from './interfaces/HTTPClient'; | ||
//Worked out from: https://github.com/TeamNewPipe/NewPipeExtractor | ||
import helpers from "./fetchers/helpers"; | ||
import { HTTPRequestMethod } from "./interfaces/HTTPClient"; | ||
import { WrappedHTTPClient } from "./WrappedHTTPClient"; | ||
const jsVarStr = '[a-zA-Z_\\$][a-zA-Z_0-9]*'; | ||
const jsSingleQuoteStr = `'[^'\\\\]*(:?\\\\[\\s\\S][^'\\\\]*)*'`; | ||
const jsDoubleQuoteStr = `"[^"\\\\]*(:?\\\\[\\s\\S][^"\\\\]*)*"`; | ||
const jsQuoteStr = `(?:${jsSingleQuoteStr}|${jsDoubleQuoteStr})`; | ||
const jsKeyStr = `(?:${jsVarStr}|${jsQuoteStr})`; | ||
const jsPropStr = `(?:\\.${jsVarStr}|\\[${jsQuoteStr}\\])`; | ||
const jsEmptyStr = `(?:''|"")`; | ||
const reverseStr = ':function\\(a\\)\\{' + | ||
'(?:return )?a\\.reverse\\(\\)' + | ||
'\\}'; | ||
const sliceStr = ':function\\(a,b\\)\\{' + | ||
'return a\\.slice\\(b\\)' + | ||
'\\}'; | ||
const spliceStr = ':function\\(a,b\\)\\{' + | ||
'a\\.splice\\(0,b\\)' + | ||
'\\}'; | ||
const swapStr = ':function\\(a,b\\)\\{' + | ||
'var c=a\\[0\\];a\\[0\\]=a\\[b(?:%a\\.length)?\\];a\\[b(?:%a\\.length)?\\]=c(?:;return a)?' + | ||
'\\}'; | ||
const actionsObjRegexp = new RegExp( | ||
`var (${jsVarStr})=\\{((?:(?:` + | ||
jsKeyStr + reverseStr + '|' + | ||
jsKeyStr + sliceStr + '|' + | ||
jsKeyStr + spliceStr + '|' + | ||
jsKeyStr + swapStr + | ||
'),?\\r?\\n?)+)\\};' | ||
); | ||
const actionsFuncRegexp = new RegExp(`function(?: ${jsVarStr})?\\(a\\)\\{` + | ||
`a=a\\.split\\(${jsEmptyStr}\\);\\s*` + | ||
`((?:(?:a=)?${jsVarStr}` + | ||
jsPropStr + | ||
'\\(a,\\d+\\);)+)' + | ||
`return a\\.join\\(${jsEmptyStr}\\)` + | ||
'\\}' | ||
); | ||
const reverseRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${reverseStr}`, 'm'); | ||
const sliceRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${sliceStr}`, 'm'); | ||
const spliceRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${spliceStr}`, 'm'); | ||
const swapRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${swapStr}`, 'm'); | ||
const REGEXES = [ | ||
new RegExp("(?:\\b|[^a-zA-Z0-9$])([a-zA-Z0-9$]{2,})\\s*=\\s*function\\(\\s*a\\s*\\)" + "\\s*\\{\\s*a\\s*=\\s*a\\.split\\(\\s*\"\"\\s*\\)"), | ||
new RegExp("\\bm=([a-zA-Z0-9$]{2,})\\(decodeURIComponent\\(h\\.s\\)\\)"), | ||
new RegExp("\\bc&&\\(c=([a-zA-Z0-9$]{2,})\\(decodeURIComponent\\(c\\)\\)"), | ||
new RegExp("([\\w$]+)\\s*=\\s*function\\((\\w+)\\)\\{\\s*\\2=\\s*\\2\\.split\\(\"\"\\)\\s*;"), | ||
new RegExp("\\b([\\w$]{2,})\\s*=\\s*function\\((\\w+)\\)\\{\\s*\\2=\\s*\\2\\.split\\(\"\"\\)\\s*;"), | ||
new RegExp("\\bc\\s*&&\\s*d\\.set\\([^,]+\\s*,\\s*(:encodeURIComponent\\s*\\()([a-zA-Z0-9$]+)\\(") | ||
]; | ||
export async function decipher(formats: Array<any>, playerURL: string, httpClient: WrappedHTTPClient) { | ||
const playerResults = await httpClient.request({ | ||
method: HTTPRequestMethod.GET, | ||
url: playerURL | ||
}); | ||
export class CiphService { | ||
const playerJS = playerResults.data; | ||
constructor(public httpClient: HTTPClient) {} | ||
cache = new Map(); | ||
async getTokens(html5playerfile: any, options: any) { | ||
const cachedTokens = this.cache.get(html5playerfile); | ||
const response = await this.httpClient.request({ | ||
method: HTTPRequestMethod.GET, | ||
url: html5playerfile | ||
}); | ||
const tokens = this.extractActions(response.data); | ||
if (!tokens || !tokens.length) { | ||
throw new Error('Could not extract signature deciphering actions'); | ||
let deobfuscateFunctionName:any = ""; | ||
for(const reg of REGEXES) { | ||
deobfuscateFunctionName = matchGroup1(reg, playerJS); | ||
if(deobfuscateFunctionName) { | ||
break; | ||
} | ||
this.cache.set(html5playerfile, tokens); | ||
return tokens; | ||
} | ||
extractActions(body: any) { | ||
const objResult = actionsObjRegexp.exec(body); | ||
const funcResult = actionsFuncRegexp.exec(body); | ||
if (!objResult || !funcResult) { return null; } | ||
const functionPattern = new RegExp("(" + deobfuscateFunctionName.replace("$", "\\$") + "=function\\([a-zA-Z0-9_]+\\)\\{.+?\\})"); | ||
const deobfuscateFunction = "var " + matchGroup1(functionPattern, playerJS) + ";"; | ||
const obj = objResult[1].replace(/\$/g, '\\$'); | ||
const objBody = objResult[2].replace(/\$/g, '\\$'); | ||
const funcBody = funcResult[1].replace(/\$/g, '\\$'); | ||
const helperObjectName = matchGroup1(new RegExp(";([A-Za-z0-9_\\$]{2})\\...\\("), deobfuscateFunction); | ||
const helperPattern = new RegExp("(var " + helperObjectName + "=\\{.+?\\}\\};)"); | ||
const helperObject = matchGroup1(helperPattern, helpers.replaceAll("\n", "", playerJS)); | ||
const finalFunc = eval(`(function getDecipherFunction() { | ||
` + helperObject + ` | ||
` + deobfuscateFunction + ` | ||
let result = reverseRegexp.exec(objBody); | ||
const reverseKey = result && result[1] | ||
.replace(/\$/g, '\\$') | ||
.replace(/\$|^'|^"|'$|"$/g, ''); | ||
result = sliceRegexp.exec(objBody); | ||
const sliceKey = result && result[1] | ||
.replace(/\$/g, '\\$') | ||
.replace(/\$|^'|^"|'$|"$/g, ''); | ||
result = spliceRegexp.exec(objBody); | ||
const spliceKey = result && result[1] | ||
.replace(/\$/g, '\\$') | ||
.replace(/\$|^'|^"|'$|"$/g, ''); | ||
result = swapRegexp.exec(objBody); | ||
const swapKey = result && result[1] | ||
.replace(/\$/g, '\\$') | ||
.replace(/\$|^'|^"|'$|"$/g, ''); | ||
return (val) => ` + deobfuscateFunctionName + `; | ||
})()`)(); | ||
const keys = `(${[reverseKey, sliceKey, spliceKey, swapKey].join('|')})`; | ||
const myreg = '(?:a=)?' + obj + | ||
`(?:\\.${keys}|\\['${keys}'\\]|\\["${keys}"\\])` + | ||
'\\(a,(\\d+)\\)'; | ||
const tokenizeRegexp = new RegExp(myreg, 'g'); | ||
const tokens = []; | ||
// tslint:disable-next-line:no-conditional-assignment | ||
while ((result = tokenizeRegexp.exec(funcBody)) !== null) { | ||
const key = result[1] || result[2] || result[3]; | ||
switch (key) { | ||
case swapKey: | ||
tokens.push('w' + result[4]); | ||
break; | ||
case reverseKey: | ||
tokens.push('r'); | ||
break; | ||
case sliceKey: | ||
tokens.push('s' + result[4]); | ||
break; | ||
case spliceKey: | ||
tokens.push('p' + result[4]); | ||
break; | ||
} | ||
for(const format of formats) { | ||
if(format.signatureCipher) { | ||
const signatureParams = parseQuery(format.signatureCipher); | ||
const resolvedSignature = finalFunc(signatureParams.s); | ||
const finalURL = new URL(signatureParams.url); | ||
finalURL.searchParams.set(signatureParams.sp, resolvedSignature) | ||
format.url = finalURL.toString(); | ||
} | ||
return tokens; | ||
} | ||
return formats; | ||
} | ||
async decipherFormats(formats: any, html5player: any, options: any) { | ||
const decipheredFormats: any = []; | ||
const tokens = await this.getTokens(html5player, options); | ||
function matchGroup1(regex: RegExp, str: string) { | ||
const res = regex.exec(str); | ||
if(!res) return ""; | ||
return res[1]; | ||
} | ||
formats.forEach((format: any) => { | ||
const cipher = format.signatureCipher || format.cipher; | ||
if (cipher) { | ||
Object.assign(format, querystring.parse(cipher)); | ||
delete format.signatureCipher; | ||
delete format.cipher; | ||
} | ||
const sig = tokens && format.s ? this.decipher(tokens, format.s) : null; | ||
this.setDownloadURL(format, sig); | ||
decipheredFormats.push(format); | ||
}); | ||
return decipheredFormats; | ||
function parseQuery(queryString:string) { | ||
var query:any = {}; | ||
var pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&'); | ||
for (var i = 0; i < pairs.length; i++) { | ||
var pair = pairs[i].split('='); | ||
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || ''); | ||
} | ||
decipher = (tokens: any, sig: any) => { | ||
sig = sig.split(''); | ||
for (let i = 0, len = tokens.length; i < len; i++) { | ||
// tslint:disable-next-line:prefer-const one-variable-per-declaration | ||
let token = tokens[i], pos; | ||
switch (token[0]) { | ||
case 'r': | ||
sig = sig.reverse(); | ||
break; | ||
case 'w': | ||
// tslint:disable-next-line:no-bitwise | ||
pos = ~~token.slice(1); | ||
const first = sig[0]; | ||
sig[0] = sig[pos % sig.length]; | ||
sig[pos] = first; | ||
break; | ||
case 's': | ||
// tslint:disable-next-line:no-bitwise | ||
pos = ~~token.slice(1); | ||
sig = sig.slice(pos); | ||
break; | ||
case 'p': | ||
// tslint:disable-next-line:no-bitwise | ||
pos = ~~token.slice(1); | ||
sig.splice(0, pos); | ||
break; | ||
} | ||
} | ||
return sig.join(''); | ||
} | ||
setDownloadURL(format: any, sig: any) { | ||
let decodedUrl; | ||
if (format.url) { | ||
decodedUrl = format.url; | ||
} else { | ||
return; | ||
} | ||
try { | ||
decodedUrl = decodeURIComponent(decodedUrl); | ||
} catch (err) { | ||
return; | ||
} | ||
// Make some adjustments to the final url. | ||
const parsedUrl = url.parse(decodedUrl, true); | ||
// Deleting the `search` part is necessary otherwise changes to | ||
// `query` won't reflect when running `url.format()` | ||
// @ts-ignore | ||
delete parsedUrl.search; | ||
const query = parsedUrl.query; | ||
// This is needed for a speedier download. | ||
// See https://github.com/fent/node-ytdl-core/issues/127 | ||
query.ratebypass = 'yes'; | ||
if (sig) { | ||
// When YouTube provides a `sp` parameter the signature `sig` must go | ||
// into the parameter it specifies. | ||
// See https://github.com/fent/node-ytdl-core/issues/417 | ||
query[format.sp || 'signature'] = sig; | ||
} | ||
format.url = url.format(parsedUrl); | ||
} | ||
return query; | ||
} |
@@ -1,6 +0,6 @@ | ||
import { ENDPOINT_COMMENT_CREATE, ENDPOINT_DISLIKE, ENDPOINT_LIKE, ENDPOINT_NEXT, ENDPOINT_PLAYER, ENDPOINT_REMOVELIKE, ENDPOINT_WATCHPAGE } from "../constants"; | ||
import { DEFAULT_CLIENT_VERSION, DEFAULT_USER_AGENT, ENDPOINT_COMMENT_CREATE, ENDPOINT_DISLIKE, ENDPOINT_LIKE, ENDPOINT_NEXT, ENDPOINT_PLAYER, ENDPOINT_REMOVELIKE, ENDPOINT_WATCHPAGE } from "../constants"; | ||
import helpers from "../fetchers/helpers"; | ||
import { CommentSectionContinuatedList, ContinuatedList, WrappedHTTPClient, Channel, Thumbnail, CaptionTrack, CommentThread, Format } from "../main"; | ||
import { HTTPRequestMethod } from "./HTTPClient"; | ||
import { CiphService } from "../formatsChipher"; | ||
import * as formatsChipher from "../formatsChipher"; | ||
@@ -236,39 +236,29 @@ export class Video { | ||
await this.loadFormats(); | ||
} | ||
async loadFormats() { | ||
const playerResponse = await this.httpclient.request({ | ||
method: HTTPRequestMethod.POST, | ||
url: ENDPOINT_PLAYER, | ||
data: { | ||
videoId: this.videoId, | ||
racyCheckOk: false, | ||
contentCheckOk: false, | ||
playbackContext: { | ||
contentPlaybackContent: { | ||
currentUrl: "/watch?v=6Dh-RL__uN4", | ||
autonavState: "STATE_OFF", | ||
autoCaptionsDefaultOn: false, | ||
html5Preference: "HTML5_PREF_WANTS", | ||
lactMilliseconds: "-1", | ||
referer: "https://www.youtube.com/", | ||
signatureTimestamp: 19095, | ||
splay: false, | ||
vis: 0 | ||
} | ||
} | ||
} | ||
}); | ||
const playerJSON = await JSON.parse(playerResponse.data); | ||
const formats = helpers.recursiveSearchForKey("adaptiveFormats", playerJSON)[0]; | ||
const watchURL = new URL(ENDPOINT_WATCHPAGE); | ||
watchURL.searchParams.set("v", this.videoId); | ||
const playPage = await this.httpclient.request({ | ||
const playPage = await this.httpclient.client.request({ | ||
method: HTTPRequestMethod.GET, | ||
url: watchURL.toString() | ||
url: watchURL.toString(), | ||
headers: { | ||
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" | ||
} | ||
}); | ||
const html = playPage.data; | ||
const varFind = html.indexOf("var ytInitialPlayerResponse ="); //Locate the Var Definition | ||
const scriptStart = helpers.getIndexAfter(">", helpers.getIndexBefore("<script", varFind, html), html) + 1; //Get Script Tag Before | ||
const scriptEnd = helpers.getIndexAfter("</script>", varFind, html); //Get Script Tag After | ||
const script = html.substring(scriptStart, scriptEnd); //Get Script (between both Tags) | ||
const scriptFunc = new Function(script + " return ytInitialPlayerResponse;"); //Parse the Functions Inside | ||
const playerJSON = scriptFunc(); //Get the Information | ||
const formats = helpers.recursiveSearchForKey("adaptiveFormats", playerJSON)[0]; | ||
let playerScript:any = /<script\s+src="([^"]+)"(?:\s+type="text\/javascript")?\s+name="player_ias\/base"\s*>|"jsUrl":"([^"]+)"/.exec(html); | ||
@@ -278,4 +268,3 @@ playerScript = playerScript[2] || playerScript[1]; | ||
const cipher = new CiphService(this.httpclient); | ||
const resolvedFormats = await cipher.decipherFormats(formats, playerURLString, {}); | ||
const resolvedFormats = await formatsChipher.decipher(formats, playerURLString, this.httpclient); | ||
@@ -282,0 +271,0 @@ this.formats = []; |
@@ -115,2 +115,6 @@ import { Authenticator } from "./Authenticator"; | ||
getCookies() { | ||
return this.wrappedHttpClient.cookieString; | ||
} | ||
throwErrorIfNotReady(){ | ||
@@ -117,0 +121,0 @@ if(!this.storageAdapter) throw new Error("The provided Storage Adapter was invalid"); |
@@ -22,2 +22,3 @@ import { Explorer } from "./fetchers/Explorer"; | ||
import { default as IYoutube } from "./IYoutube"; | ||
import { HTTPRequestOptions, HTTPRequestMethod, HTTPResponse } from "./interfaces/HTTPClient"; | ||
@@ -48,2 +49,5 @@ | ||
export { Format as Format } | ||
export { HTTPRequestMethod as HTTPRequestMethod } | ||
export { HTTPRequestOptions as HTTPRequestOptions } | ||
export { HTTPResponse as HTTPResponse } | ||
@@ -50,0 +54,0 @@ //Skips Webpack checks |
@@ -106,2 +106,2 @@ import { CONSOLE_COLORS, DEBUG, DEFAULT_API_KEY, DEFAULT_CLIENT_NAME, DEFAULT_CLIENT_VERSION, DEFAULT_CONTEXT, DEFAULT_USER_AGENT } from "./constants"; | ||
return resString; | ||
} | ||
} |
@@ -10,3 +10,3 @@ ## Todo | ||
- [X] Fetch all Information (Youtube Player Response) | ||
- [ ] Stream URL Decryption & Video Formats | ||
- [X] Stream URL Decryption & Video/Audio Formats | ||
- [X] Get Comment Section | ||
@@ -13,0 +13,0 @@ - [X] Like and Dislike Comments |
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 2 instances in 1 package
237946
4795
3