Skip to content
Snippets Groups Projects
YoutubeScript.js 171 KiB
Newer Older
Koen's avatar
Koen committed


//#region Basic Extractors
function extractText_String(item) {
    if(typeof item === 'string')
        return item;
    if(item?.simpleText)
        return item.simpleText;
    if(item?.runs)
        return extractRuns_String(item.runs);

	if(item)
		log("Unknown string object: " + JSON.stringify(item, null, "   "));
    return null;
}
function extractRuns_String(runs) {
	if(!runs)
		return null;

	let str = "";
	for(let runi = 0; runi < runs.length; runi++) {
		const run = runs[runi];
		if(run.text)
			str += run.text;
		else if(run.emoji?.image?.accessibility?.accessibilityData?.label)
			str += "__" + run.emoji?.image?.accessibility?.accessibilityData?.label + "__"
	}
	return str;
}
function extractRuns_Html(runs) {
	if(!runs)
		return null;

	let str = "";
	for(let runi = 0; runi < runs.length; runi++) {
		const run = runs[runi];
		if(run.text)
			str += run.text;
	}
	return str;
}
function extractRuns_Url(runs) {
	for(let runi = 0; runi < runs.length; runi++) {
		const run = runs[runi];

		if(run.navigationEndpoint && run.navigationEndpoint.browseEndpoint && run.navigationEndpoint.browseEndpoint.canonicalBaseUrl)
			return URL_BASE + run.navigationEndpoint.browseEndpoint.canonicalBaseUrl;
	}
}

function extractNavigationEndpoint_Url(navEndpoint, baseUrl) {
    if(!baseUrl)
        baseUrl = URL_BASE;
    if(!navEndpoint)
Koen's avatar
Koen committed
		return null;
	if(navEndpoint?.browseEndpoint?.browseId && navEndpoint?.browseEndpoint?.canonicalBaseUrl && navEndpoint.browseEndpoint.canonicalBaseUrl.startsWith("/@"))
		return baseUrl + "/channel/" + navEndpoint?.browseEndpoint?.browseId;
Koen's avatar
Koen committed
    if(navEndpoint?.browseEndpoint?.canonicalBaseUrl)
        return baseUrl + navEndpoint?.browseEndpoint?.canonicalBaseUrl;
    if(navEndpoint.commandMetadata?.webCommandMetadata?.url)
        return baseUrl + navEndpoint.commandMetadata?.webCommandMetadata?.url;
    return null;
}

function extractAgoTextRuns_Timestamp(runs) {
	const runStr = (typeof runs === "string") ? runs : extractRuns_String(runs);
Koen's avatar
Koen committed
	return extractAgoText_Timestamp(runStr);
}
function extractAgoText_Timestamp(str) {
Kelvin's avatar
Kelvin committed
	if(!str)
		return 0;
Koen's avatar
Koen committed
	const match = str.match(REGEX_HUMAN_AGO);
	if(!match)
		return 0;
	const value = parseInt(match[1]);
	const now = parseInt(new Date().getTime() / 1000);
	switch(match[2]) {
		case "second":
		case "seconds":
			return now - value;
		case "minute":
		case "minutes":
			return now - value * 60;
		case "hour":
		case "hours":
			return now - value * 60 * 60;
		case "day":
		case "days":
			return now - value * 60 * 60 * 24;
		case "week":
		case "weeks":
			return now - value * 60 * 60 * 24 * 7;
		case "month":
		case "months":
			return now - value * 60 * 60 * 24 * 30; //For now it will suffice
		case "year":
		case "years":
			return now - value * 60 * 60 * 24 * 365;
		default:
	        if(bridge.devSubmit) bridge.devSubmit("extractAgoText_Timestamp - Unknown time type: " + match[2], match[2]);
Koen's avatar
Koen committed
			throw new ScriptException("Unknown time type: " + match[2]);
	}
}
function extractRuns_ViewerCount(runs) {
	if(runs && runs.length > 0) {
		const item = runs[0].text.replaceAll(".", "").replaceAll(",", "");
		if(isNaN(item))
			return -1;
		return parseInt(item);
	}
	return -1;
}
function extractHumanTime_Seconds(str) {
Kelvin's avatar
Kelvin committed
	if(!str)
		return 0;
Koen's avatar
Koen committed
	if(str.indexOf(" ") >= 0)
		str = str.split(" ")[0];
	const parts = str.split(":");
	let scale = 1;
	let seconds = 0;
	for(let i = parts.length-1; i >= 0; i--) {
		if(isNaN(parts[i]))
			return seconds;
		seconds += parseInt(parts[i]) * scale;
		scale *= 60;
	}
	return parseInt(seconds);
}
function extractFirstNumber_Integer(str) {
	if(str) {
		const parts = str.split(' ');
		if(parts && parts.length > 0) {
			const num = parts[0].replaceAll(".","").replaceAll(",","");
			if(isNaN(num))
				return -1;
			return parseInt(num);
		}
	}
	return -1;
}
function extractHumanNumber_Integer(str) {
	if(!str)
		return -1;
	const match = str.match(REGEX_HUMAN_NUMBER);
	if(!match)
		return extractFirstNumber_Integer(str);

	const value = parseFloat(match[1]);
	
	switch(match[2]) {
		case "T":
			return parseInt(1000000000000 * value);
		case "B":
			return parseInt(1000000000 * value);
		case "M":
			return parseInt(1000000 * value);
		case "K":
			return parseInt(1000 * value);
		default:
			return parseInt(value);
	}
}
function extractDate_Timestamp(dateStr) {
	if(!dateStr)
		return -1;
	if(dateStr.indexOf("ago") > 0)
		return extractAgoText_Timestamp(dateStr);

	let matchDate = dateStr.match(REGEX_DATE_HUMAN);
	if(matchDate) return extractHumanDate_Timestamp(matchDate.slice(1));
	matchDate = dateStr.match(REGEX_DATE_EU);
	if(matchDate) return new Date(matchDate[0]).getTime() / 1000;
	matchDate = dateStr.match(REGEX_DATE_EU);
	if(matchDate) return new Date(matchDate[0]).getTime() / 1000;
	return -1;
}
function extractHumanDate_Timestamp(dateParts) {
	if(dateParts.length != 3)
		return -1;
	let day = -1;
	let month = -1;
	let year = -1;
	for(let i = 0; i < dateParts.length; i++) {
		const part = dateParts[i];
		if(part.length > 2) {
			const newMonth = monthNameToNumber(part);
			if(newMonth > 0)
				month = newMonth;
		}
		if(part.length == 4 && !isNaN(part))
			year = parseInt(part);
		if(part.length <= 2 && !isNaN(part))
			day = parseInt(part);
	}
	return (day > 0 && month > 0 && year > 0) ? 
		new Date(year + "-" + month + "-" + day).getTime() / 1000 : 
		-1;
}

function escapeUnicode(str) {
	if(!str)
		return str;
	return str.replace("\\u0026", "&");
}

Koen's avatar
Koen committed
//#endregion

//#region Filters
const FILTER_DATE_HOUR = 1;
const FILTER_DATE_DAY = 2;
const FILTER_DATE_WEEK = 3;
const FILTER_DATE_MONTH = 4;
const FILTER_DATE_YEAR = 5;

const FILTER_DURATION_4MIN = 1;
const FILTER_DURATION_4_20MIN = 3;
const FILTER_DURATION_20MIN = 2;

const FILTER_HD = 32;
const FILTER_SUBS = 40;
const FILTER_LIVE = 64;
const FILTER_4K = 112;
const FILTER_CreativeCommons = 48;
const FILTER_360 = 120
const FILTER_VR = 208;
const FILTER_3D = 56;
const FILTER_HDR = 200
const FILTERS = [
	{
		id: "date",
		name: "Upload Date",
		isMultiSelect: false,
		filters: [
			new FilterCapability("Last Hour", FILTER_DATE_HOUR, Type.Date.LastHour),
			new FilterCapability("This Day", FILTER_DATE_DAY, Type.Date.Today),
			new FilterCapability("This Week", FILTER_DATE_WEEK, Type.Date.LastWeek),
			new FilterCapability("This Month", FILTER_DATE_MONTH, Type.Date.LastMonth),
			new FilterCapability("This Year", FILTER_DATE_YEAR, Type.Date.LastYear),
		]
	},
	{
		id: "duration",
		name: "Duration",
		isMultiSelect: false,
		filters: [
			new FilterCapability("Under 4 minutes", FILTER_DURATION_4MIN, Type.Duration.Short),
			new FilterCapability("4-20 minutes", FILTER_DURATION_4_20MIN, Type.Duration.Medium),
			new FilterCapability("Over 20 minutes", FILTER_DURATION_20MIN, Type.Duration.Long)
		]
	},
	{
		id: "features",
		name: "Features",
		isMultiSelect: true,
		filters: [
			new FilterCapability("HD", FILTER_HD),
			new FilterCapability("4K", FILTER_4K),
			new FilterCapability("HDR", FILTER_HDR),
			new FilterCapability("Subtitles", FILTER_SUBS),
			new FilterCapability("Live", FILTER_LIVE),
			new FilterCapability("Creative Commons", FILTER_CreativeCommons),
			new FilterCapability("VR", FILTER_VR),
			new FilterCapability("3D", FILTER_3D),
			new FilterCapability("360", FILTER_360)
		]
	}
]

const SORT_RELEVANCE = 18;
const SORT_DATE = 2;
const SORT_VIEWS = 3;
const SORT_RATING = 1;

const TYPE_VIDEO = 1;
const TYPE_CHANNEL = 2;
const TYPE_PLAYLIST = 3;
const TYPE_MOVIES = 4;

const PREFIX_TYPE = 16;
const PREFIX_LENGTH = 18;
const PREFIX_ORDER = 8;
const PREFIX_DATE = 8;
const PREFIX_DURATION = 24;


function sortToByte(sort) {
	switch(sort) {
		case Type.Order.Chronological:
			return SORT_DATE;
		case SORT_RATING_STRING:
			return SORT_RATING;
		case SORT_VIEWS_STRING:
			return SORT_VIEWS;
		default:
			throw new ScriptException("Unknown sort");
	}
}

function searchQueryToSP(sort, type, filters) {
	if(!type)
		type = TYPE_VIDEO;

	let filter_date = (filters?.date && filters.date.length > 0) ? filters.date[0] : null;
	let filter_duration = (filters?.duration && filters.duration.length > 0) ? filters.duration[0] : null;
	let filter_features = filters?.features ?? [];
	
	const sortByte = sort ? sortToByte(sort) : null;//SORT_RELEVANCE;

	let arrLength = 0;
	let filterLength = 0;
	if(sortByte)
		arrLength += 2;
	if(type) {
		filterLength += 2;
		arrLength += 2;
	}
	if(filter_date) {
		filterLength += 2;
		arrLength += 2;
	}
	if(filter_duration) {
		filterLength += 2;
		arrLength += 2;
	}
	if(filter_features.length > 0) {
		for(let i = 0; i < filter_features.length; i++) {
			arrLength += 2;
			filterLength += 2;
			if(filter_features[i] > 128) {
				arrLength += 1;
				filterLength += 1;
			}
		}
	}
	if(filterLength > 0)
		arrLength += 2;
	
	const array = new Uint8Array(arrLength);
	let index = 0;


	if(sortByte) {
		array[index] = PREFIX_ORDER;
		array[index + 1] = sortByte;
		index += 2;
	}
	if(filterLength > 0) {
		array[index] = PREFIX_LENGTH;
		array[index + 1] = filterLength;
		index += 2;
	}
	if(filter_date) {
		array[index] = PREFIX_DATE;
		array[index + 1] = filter_date;
		index += 2;
	}
	if(filter_duration) {
		array[index] = PREFIX_DURATION;
		array[index + 1] = filter_duration;
		index += 2;
	}
	if(type) {
		array[index] = PREFIX_TYPE;
		array[index + 1] = type;
		index += 2;
	}
	for(let i = 0; i < filter_features.length; i++) {
		array[index] = filter_features[i];
		array[index + 1] = 1;
		index += 2;
		if(filter_features[i] > 128) {
			array[index] = 1;
			index += 1;
		}
	}

	return utility.toBase64(array);
}

//#endregion


//#region Utility
const htmlEncodedCharacters = {
	"amp": "&",
	"lt": "<",
	"gt": ">",
	"quot": "\"",
	"apos": "'"
}
function decodeHtml(text) {
	return text.replace(/(?:&amp;|&)#([0-9]*);/gm, function(match, dec) {
		return String.fromCharCode(dec);
	}).replace(/&([a-z]*);(#.*?;)?/gm, function(match, c){
		if(htmlEncodedCharacters[c])
			return htmlEncodedCharacters[c];
		return c;
	});
}

function monthNameToNumber(month) {
	if(!month)
		return -1;
	month = month.toLowerCase();

	//Either partial or full month name
	if(month.startsWith("jan")) return 1;
	if(month.startsWith("feb")) return 2;
	if(month.startsWith("mar")) return 3;
	if(month.startsWith("apr")) return 4;
	if(month.startsWith("may")) return 5;
	if(month.startsWith("jun")) return 6;
	if(month.startsWith("jul")) return 7;
	if(month.startsWith("aug")) return 8;
	if(month.startsWith("sep")) return 9;
	if(month.startsWith("oct")) return 10;
	if(month.startsWith("nov")) return 11;
	if(month.startsWith("dec")) return 12;
	return -1;
}

const ytLangMap = {
	"ar": Language.ARABIC,
	"es": Language.SPANISH,
	"fr": Language.FRENCH,
	"hi": Language.HINDI,
	"id": Language.INDONESIAN,
	"ko": Language.KOREAN,
	"pt-BR": Language.PORTBRAZIL,
	"ru": Language.RUSSIAN,
	"th": Language.THAI,
	"tr": Language.TURKISH,
	"vi": Language.VIETNAMESE,
	"en": Language.ENGLISH,
	"en-US": Language.ENGLISH
};
function ytLangIdToLanguage(id) {
	if(!id)
		return Language.UNKNOWN;
	const langParts = id?.split(".");
Kelvin's avatar
Kelvin committed
	let langPart = (langParts && langParts.length > 0) ? langParts[0] : "";
Koen's avatar
Koen committed
	if(ytLangMap[langPart])
Kelvin's avatar
Kelvin committed
	    return ytLangMap[langPart]; //Backwards compat
	if(langPart.indexOf("-") > 0)
	   langPart = langPart.split("-")[0].trim();
	if(ytLangMap[langPart])
	    return ytLangMap[langPart]; //Backwards compat
	if(langPart && langPart.length > 0)
	    return langPart.trim();
Koen's avatar
Koen committed
	return Language.UNKNOWN;
}

function findRenderer(obj, rendererName) {
    if(!obj)
        return null;
    const keys = Object.keys(obj);
    if(!keys || keys.length == 0)
        return null;
	const objName = keys[0];
	const renderer = obj[objName];
	if(objName == rendererName)
	    return renderer;
    if(renderer.contents) {
        for(let content of renderer.contents) {
            const result = findRenderer(content, rendererName);
            if(result)
                return result;
        }
    }
    if(renderer.content)
        return findRenderer(renderer.content, rendererName);
    return null;
}

function switchKey(obj, handlers) {
	const objName = Object.keys(obj)[0];
	if(!objName) {
		if(handlers["null"])
			return handlers["null"];
		return null;
	}
	
	if(handlers[objName])
		return handlers[objName](obj[objName]);
	if(handlers["default"])
		return handlers["default"](objName);
	return null;
}

//#endregion


function validateContinuation(reqcb, useAuth = false) {
	const clientContext = getClientContext(useAuth);
	const result = reqcb();
	const append = result?.onResponseReceivedCommands ?? result?.onResponseReceivedActions;
	if(append && append.length > 0 && append[0].appendContinuationItemsAction) {
		const appendResults = append[0].appendContinuationItemsAction.continuationItems;
		if(!appendResults) {
			if(IS_TESTING)
				console.log("Continuation found without items?", result);
			return [];
		}
		else
			return appendResults;
	}
Koen's avatar
Koen committed
	else if(!clientContext.INNERTUBE_CONTEXT.client.visitorData && result.responseContext?.visitorData) {
		log("[validateContinuation] No visitor data set, found visitor data in response, retrying");
		clientContext.INNERTUBE_CONTEXT.client.visitorData = result.responseContext.visitorData;
		//Retry with visitorData
		const reResult = reqcb();
		log("[validateContinuation] retry result");
		if(append && append.length > 0 && append[0].appendContinuationItemsAction) {
			const appendResults = append[0].appendContinuationItemsAction.continuationItems;
			if(!appendResults) {
				if(IS_TESTING)
					console.log("Continuation found without items?", result);
				return [];
			}
			else
				return appendResults;
		}
Koen's avatar
Koen committed
		else
			return [];
	}
	else
		return [];
}

//#region Cipher/Decryption

var _cipherDecode = {

};
var _nDecrypt = {
	
};
var _sts = {
	
};
const REGEX_CIPHERS = [
	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$]+)\\(")
];
Kelvin's avatar
Kelvin committed
const REGEX_DECRYPT_N_VARIANTS = [
	/\.get\(\"n\"\)\)&&\([a-zA-Z0-9$_]=([a-zA-Z0-9$_]+)(?:\[(\d+)])?\([a-zA-Z0-9$_]\)/,
	/[a-zA-Z0-9$_]+=String\.fromCharCode\(110\),[a-zA-Z0-9$_]+=[a-zA-Z0-9$_]+\.get\([a-zA-Z0-9$_]+\)\)&&\([a-zA-Z0-9$_]=([a-zA-Z0-9$_]+)(?:\[(\d+)])?\([a-zA-Z0-9$_]\)/,
Kelvin's avatar
Kelvin committed
	/[a-zA-Z]+="[n]+"\[.+\],[a-zA-Z0-9$_]+=[a-zA-Z0-9$_]+\.get\([a-zA-Z0-9$_]+\)\)&&\([a-zA-Z0-9$_]=([a-zA-Z0-9$_]+)(?:\[(\d+)])?\([a-zA-Z0-9$_]\)/,
	/\/file\/index\.m3u8.+?[a-zA-Z0-9$_]=([a-zA-Z0-9$_]+)(?:\[(\d+)])?\([a-zA-Z0-9$_]\)/
Koen's avatar
Koen committed
const REGEX_PARAM_N = new RegExp("[?&]n=([^&]*)");
const STS_REGEX = new RegExp("signatureTimestamp[=:](\\d+)");

source.decryptUrlTest = function(encrypted) {
	prepareCipher();

	let url = decryptUrlN(encrypted.url, true);
	if(!url)
	    url = decryptUrl(encrypted.cipher, true);
	if(!url)
		url = decryptUrl(encrypted.signatureCipher, true);
	return url;
}
source.decryptUrlTestN = function(n) {
	prepareCipher();
	let url = "https://whatever.com/asdgdsag?a=b&n=" + n + "&u=asd"

	return decryptUrlN(url, true);
}

function decryptUrl(encrypted, jsUrl, doLogging) {
	if(!encrypted) return null;

	const query = parseQueryString(encrypted);
	const baseUrl = query.url;
	const sigKey = query.sp;
	const sigValue = decodeCipher(decodeURIComponent(query.s), jsUrl);

	let decryptedUrl = decodeURIComponent(baseUrl) + "&" + sigKey + "=" + sigValue;

	if(doLogging) {
		log("SigKey: " + sigKey);
		log("SigValue: " + sigValue);
		log("Decrypted: " + decryptedUrl);
	}
	return decryptUrlN(decryptedUrl, jsUrl, doLogging);
}
function decryptUrlN(url, jsUrl, doLogging) {
	const nParamMatch = REGEX_PARAM_N.exec(url);
	if(nParamMatch) {
		const encryptedN = nParamMatch[1];
		const decryptedN = decryptN(encryptedN, jsUrl);

		if(doLogging) {
			log("Encrypt URL:" + url);
			log("NParam Found: " + encryptedN + " (length:" + encryptedN.length + ")");
			log("NParam Decrypted: " + decryptedN + " (size:" + decryptedN.length + ")");
			log("Decrypted URL:" + url.replace(encryptedN, decryptedN));
		}

		url = url.replace(encryptedN, decryptedN);
	}
	else if(doLogging)
		log("No NParam found in (" + url + ")");
	return url;
}
function decodeCipher(cipher, jsUrl) {
	if(!_cipherDecode[jsUrl])
		throw new ScriptException("Cipher decoder was not available [" + jsUrl + "]");
	return _cipherDecode[jsUrl](cipher);
}
function decryptN(encryptedN, jsUrl) {
	if(!_nDecrypt[jsUrl])
		throw new ScriptException("N Decryptor was not available [" + jsUrl + "]");
	return _nDecrypt[jsUrl](encryptedN);
}
Kelvin's avatar
Kelvin committed
function testCipher(hash) {
	const jsUrl = CIPHER_TEST_PREFIX + hash + CIPHER_TEST_SUFFIX;
	try{
		const result = prepareCipher(jsUrl);
		clearCipher(jsUrl);
		return {
			success: result,
			exception: ""
		};
	}
	catch(ex) {
		return {
			success: false,
			exception: ex
		};
	}
}
source.testCipher = testCipher;
Koen's avatar
Koen committed
function testCiphers() {
	let testResults = [];
	for(hash of CIPHER_TEST_HASHES) {
		const jsUrl = CIPHER_TEST_PREFIX + hash + CIPHER_TEST_SUFFIX;
		try{
			if(prepareCipher(jsUrl))
				testResults.push("CipherTest [" + hash + "]: PASSED");
			else 
				testResults.push("CipherTest [" + hash + "]: FAIL");
		}
		catch(ex) {
			testResults.push(["CipherTest [" + hash + "]: FAIL", ex]);
		}
		clearCipher(jsUrl);
	}
	for(result of testResults) {
		if(result.constructor === Array)
			console.log(result[0], result[1]);
		else
			console.log(result);
	}
}
source.testCiphers = testCiphers;
function prepareCipher(jsUrl) {
	if(_cipherDecode[jsUrl])
		return false;//_cipherDecode[jsUrl];
	log("New JS Url found: [" + jsUrl + "], fetching new js (total: " + (Object.keys(_cipherDecode).length + 1) + ")");

	try{
		const playerCodeResp = http.GET(URL_BASE + jsUrl, {});
		if(!playerCodeResp.isOk) {
	        if(bridge.devSubmit) bridge.devSubmit("prepareCipher - Failed to get player js", jsUrl);
Koen's avatar
Koen committed
			throw new ScriptException("Failed to get player js");
Koen's avatar
Koen committed
		console.log("Javascript Url: " + URL_BASE + jsUrl);
		const playerCode = playerCodeResp.body;

		const cipherFunctionCode = getCipherFunctionCode(playerCode, jsUrl);
		console.log("DecodeCipher Function: " + cipherFunctionCode);
		_cipherDecode[jsUrl] = eval(cipherFunctionCode);

		const decryptFunctionCode = getNDecryptorFunctionCode(playerCode, jsUrl);
		console.log("DecryptN Function: " + decryptFunctionCode);
		_nDecrypt[jsUrl] = eval(decryptFunctionCode);

		const stsMatch = playerCode.match(STS_REGEX);
		console.log("stsMatch: " + stsMatch);
		if (stsMatch !== null && stsMatch.length > 1) {
			const sts = stsMatch[1];
			_sts[jsUrl] = sts;
			console.log("sts: " + sts);
		}

		return true;//_cipherDecode[jsUrl];
	}
	catch(ex) {
		clearCipher(jsUrl);
        if(bridge.devSubmit) bridge.devSubmit("prepareCipher - Failed to get Cipher due to: " + ex, jsUrl);
Koen's avatar
Koen committed
		throw new ScriptException("Failed to get Cipher due to: " + ex);
	}
}
source.prepareCipher = prepareCipher;
function clearCipher(jsUrl) {
    if(_cipherDecode[jsUrl])
        _cipherDecode[jsUrl] = undefined;
    if(_nDecrypt[jsUrl])
        _nDecrypt[jsUrl] = undefined;
}
function getNDecryptorFunctionCode(code, jsUrl) {
	if(_nDecrypt[jsUrl])
		return _nDecrypt[jsUrl];
Kelvin's avatar
Kelvin committed
	let nDecryptFunctionArrNameMatch = undefined;
	for(let i = 0; i < REGEX_DECRYPT_N_VARIANTS.length; i++) {
		nDecryptFunctionArrNameMatch = REGEX_DECRYPT_N_VARIANTS[i].exec(code);
		if(!nDecryptFunctionArrNameMatch) {
			console.log("NDecryptor failed, trying fallback to [" + i + 2 + "]");
		}
		else
			break;
	if(!nDecryptFunctionArrNameMatch) {
        if(bridge.devSubmit) bridge.devSubmit("getNDecryptorFunctionCode - Failed to find n decryptor (name)", jsUrl);
		throw new ScriptException("Failed to find n decryptor (name)\n" + jsUrl);
Kelvin's avatar
Kelvin committed
	const nDecryptFunctionArrName = nDecryptFunctionArrNameMatch[1];
Koen's avatar
Koen committed
	const nDecryptFunctionArrIndex = parseInt(nDecryptFunctionArrNameMatch[2]);
	
Kelvin's avatar
Kelvin committed
	const nDecryptFunctionNameMatch = code.match(escapeRegex(nDecryptFunctionArrName) + "\\s*=\\s*\\[([$a-zA-Z0-9,\\(,\\)\\.]+?)]");
	if(!nDecryptFunctionNameMatch) {
        if(bridge.devSubmit) bridge.devSubmit("getNDecryptorFunctionCode - Failed to find n decryptor (array)", jsUrl);
		throw new ScriptException("Failed to find n decryptor (array)\n" + jsUrl);
Koen's avatar
Koen committed
	const nDecryptArray = nDecryptFunctionNameMatch[1].split(",");
	if(nDecryptArray.length <= nDecryptFunctionArrIndex) {
        if(bridge.devSubmit) bridge.devSubmit("getNDecryptorFunctionCode - Failed to find n decryptor (index)", jsUrl);
		throw new ScriptException("Failed to find n decryptor (index)\n" + jsUrl);
Kelvin's avatar
Kelvin committed
	const nDecryptFunctionName = nDecryptArray[nDecryptFunctionArrIndex]
Kelvin's avatar
Kelvin committed
	const nDecryptFunctionCodeMatches = [
		escapeRegex(nDecryptFunctionName) + "=function\\(a\\)\\{[\\s\\S]*?join\\(\\\"\\\"\\)};",
		escapeRegex(nDecryptFunctionName) + "=function\\(a\\)\\{[\\s\\S]*?join\\.call\\([a-zA-Z$_]+,\\\"\\\"\\)};",
		new RegExp(escapeRegex(nDecryptFunctionName) + "=function\\(a\\)\\{[\\s\\S]*?join\\.call\\(.*?\\).*?};", "s")
	]
	let nDecryptFunctionCodeMatch = undefined;
Kelvin's avatar
Kelvin committed
	for(let functionRegex of nDecryptFunctionCodeMatches) {
		const match = code.match(functionRegex);
		if(match && match.length > 0 && (!nDecryptFunctionCodeMatch || nDecryptFunctionCodeMatch.length > match[0].length))
			nDecryptFunctionCodeMatch = match[0];
	if(!nDecryptFunctionCodeMatch) {
        if(bridge.devSubmit) bridge.devSubmit("getNDecryptorFunctionCode - Failed to find n decryptor (code)", jsUrl, code);
		throw new ScriptException("Failed to find n decryptor (code)\n" + jsUrl);
Koen's avatar
Koen committed
	
	return "(function(){" + 
Kelvin's avatar
Kelvin committed
		"var " + nDecryptFunctionCodeMatch + "\n" +
Koen's avatar
Koen committed
		"return function decryptN(nEncrypted){ return " + nDecryptFunctionName + "(nEncrypted); } \n" +
	"})()";
}
function getCipherFunctionCode(playerCode, jsUrl) {
	if(_cipherDecode[jsUrl])
		return _cipherDecode[jsUrl];
	let cipherFunctionName = null;

	for(let i = 0; i < REGEX_CIPHERS.length; i++) {
		const match = playerCode.match(REGEX_CIPHERS[i]);
		if(match) {
			cipherFunctionName = match[1];
			break;
		}
	}
	if(!cipherFunctionName)	{
        if(bridge.devSubmit) bridge.devSubmit("getCipherFunctionCode - Failed to find cipher (name)", jsUrl);
		throw new ScriptException("Failed to find cipher (name)\n" + jsUrl);
Kelvin's avatar
Kelvin committed
	const cipherFunctionCodeMatch = playerCode.match("(" + escapeRegex(cipherFunctionName) + "=function\\([a-zA-Z0-9_]+\\)\\{.+?\\})");
Koen's avatar
Koen committed
	if(!cipherFunctionCodeMatch) {
		if(IS_TESTING)
			console.log("Failed to find cipher function in: ", playerCode);
        if(bridge.devSubmit) bridge.devSubmit("getCipherFunctionCode - Failed to find cipher (function)", jsUrl);
		throw new ScriptException("Failed to find cipher (function)\n" + jsUrl);
Koen's avatar
Koen committed
	}
	const cipherFunctionCode = cipherFunctionCodeMatch[1];
	const cipherFunctionCodeVar = "var " + cipherFunctionCode;
	const helperObjNameMatch = cipherFunctionCode.match(";([A-Za-z0-9_\\$]{2,3})\\...\\(");
	if(!helperObjNameMatch) {
		if(IS_TESTING)
			console.log("Failed to find helper name in: ", playerCode);
        if(bridge.devSubmit) bridge.devSubmit("getCipherFunctionCode - Failed to find helper (name)", jsUrl);
		throw new ScriptException("Failed to find helper (name)\n" + jsUrl);
Koen's avatar
Koen committed
	}
	if(IS_TESTING)
		console.log("Cipher Code: ", cipherFunctionCode);
	const helperObjName = helperObjNameMatch[1];
	const helperObjMatch = playerCode.match("(var " + escapeRegex(helperObjName) + "=\\{[\\s\\S]*?\\};)");
	if(!helperObjMatch) {
		if(IS_TESTING)
			console.log("Failed to find helper method [" + helperObjName + "] in: ", playerCode);
        if(bridge.devSubmit) bridge.devSubmit("getCipherFunctionCode - Failed to find helper (methods)", jsUrl);
		throw new ScriptException("Failed to extract helper (methods)\n" + jsUrl);
Koen's avatar
Koen committed
	}
	const helperObj = helperObjMatch[1];
	const functionCode = "return function decodeCipher(str){ return " + cipherFunctionName + "(str); }";

	return "(function(){" + helperObj + "\n" + 
		cipherFunctionCodeVar + "\n" +
		functionCode + "})()";
}
function escapeRegex(str) {
Kelvin's avatar
Kelvin committed
	return str?.replace("$", "\\$");
Koen's avatar
Koen committed
}

function decodeHexEncodedString(str) {
	return str.replace(/\\x([0-9A-Fa-f]{2})/g, function() {
        return String.fromCharCode(parseInt(arguments[1], 16));
    });
}

function parseQueryString(query) {
	if(query.indexOf("?") >= 0)
		query = query.substring(query.indexOf("?") + 1);

	const parts = query.split("&");
	const results = {};
	for(let i = 0; i < parts.length; i++) {
		const part = parts[i];
		const valueIndex = part.indexOf("=");
		if(valueIndex == -1)
			results[part] = true;
		else
			results[part.substring(0, valueIndex)] = part.substring(valueIndex + 1);
	}
	return results;
}
//#endregion


//#region Others
const RANDOM_CHARACTER_SET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
function randomString(length) {
	let str = "";
	for(let i = 0; i < length; i++)
		str += RANDOM_CHARACTER_SET[Math.floor(Math.random() * RANDOM_CHARACTER_SET.length)]
	return str;
}
function randomInt(start, end) {
	return Math.floor(random() * (end + start) - end);
}
//#endregion

console.log("LOADED");