newpipe-extractor-js
Version:
JavaScript/Node.js port of NewPipeExtractor
347 lines • 13.6 kB
JavaScript
;
// Advanced Throttling Parameter Decryptor for NewPipe Extractor JS
// Based on yt-dlp's n-parameter handling
// Handles YouTube's anti-bot throttling mechanisms
Object.defineProperty(exports, "__esModule", { value: true });
exports.throttlingDecryptor = exports.ThrottlingDecryptor = void 0;
/**
* Advanced Throttling Parameter Decryptor
* Handles YouTube's n-parameter obfuscation used to prevent bot access
*/
class ThrottlingDecryptor {
// Transformation helper function patterns (currently unused but kept for future enhancements)
// private static readonly TRANSFORM_PATTERNS = [
// // Reverse operation
// /\.reverse\(\)/g,
// // Splice operations
// /\.splice\(\s*(\d+)\s*,\s*(\d+)\s*\)/g,
// // Character/array manipulation
// /\[(\d+)\]\s*=\s*\[(\d+)\]/g
// ];
constructor() {
this.cache = new Map();
this.CACHE_TIMEOUT = 30 * 60 * 1000; // 30 minutes
this.MAX_CACHE_SIZE = 10;
// Clean up cache periodically
setInterval(() => this.cleanupCache(), 5 * 60 * 1000); // Every 5 minutes
}
/**
* Decrypt throttling parameter from URL
*/
async decryptThrottlingParameter(url, playerJsCode) {
try {
console.log('[ThrottlingDecryptor] Processing throttling parameter...');
// Extract n parameter from URL
const nParam = this.extractNParameter(url);
if (!nParam) {
console.log('[ThrottlingDecryptor] No n parameter found in URL');
return url;
}
console.log(`[ThrottlingDecryptor] Found n parameter: ${nParam}`);
// Get player JS code if not provided
if (!playerJsCode) {
console.log('[ThrottlingDecryptor] No player JS provided, skipping n-parameter decryption');
return url;
}
// Check cache first
const cacheKey = this.generateCacheKey(playerJsCode);
const cached = this.cache.get(cacheKey);
if (cached && this.isCacheValid(cached)) {
console.log('[ThrottlingDecryptor] Using cached throttling function');
const decryptedParam = this.applyThrottlingOperations(nParam, cached.operations);
return this.replaceNParameter(url, decryptedParam);
}
// Extract and parse throttling function
const throttlingFunction = this.extractThrottlingFunction(playerJsCode);
if (!throttlingFunction) {
console.log('[ThrottlingDecryptor] No throttling function found');
return url;
}
console.log('[ThrottlingDecryptor] Found throttling function');
// Parse operations from function
const operations = this.parseThrottlingOperations(throttlingFunction);
if (operations.length === 0) {
console.log('[ThrottlingDecryptor] No operations found in throttling function');
return url;
}
console.log(`[ThrottlingDecryptor] Parsed ${operations.length} throttling operations`);
// Cache the operations
this.cache.set(cacheKey, {
operations,
timestamp: Date.now(),
playerJsUrl: '',
success: true
});
// Apply operations to decrypt n parameter
const decryptedParam = this.applyThrottlingOperations(nParam, operations);
const decryptedUrl = this.replaceNParameter(url, decryptedParam);
console.log(`[ThrottlingDecryptor] Successfully decrypted n parameter`);
return decryptedUrl;
}
catch (error) {
console.error(`[ThrottlingDecryptor] Error: ${error.message}`);
return url; // Return original URL on error
}
}
/**
* Extract n parameter from URL
*/
extractNParameter(url) {
const nMatch = url.match(/[?&]n=([^&]+)/);
return nMatch ? decodeURIComponent(nMatch[1]) : null;
}
/**
* Replace n parameter in URL
*/
replaceNParameter(url, newValue) {
return url.replace(/([?&])n=([^&]+)/, `$1n=${encodeURIComponent(newValue)}`);
}
/**
* Extract throttling function from player JS
*/
extractThrottlingFunction(playerJs) {
for (const pattern of ThrottlingDecryptor.N_FUNCTION_PATTERNS) {
const match = pattern.exec(playerJs);
if (match) {
const functionName = match[1];
// Extract the full function
const functionPattern = new RegExp(`${this.escapeRegex(functionName)}\\s*=\\s*function\\([^)]*\\)\\s*\\{[^}]*\\}`, 'g');
const funcMatch = functionPattern.exec(playerJs);
if (funcMatch) {
return funcMatch[0];
}
// Try alternative extraction method
const altPattern = new RegExp(`function\\s+${this.escapeRegex(functionName)}\\([^)]*\\)\\s*\\{[^}]*\\}`, 'g');
const altMatch = altPattern.exec(playerJs);
if (altMatch) {
return altMatch[0];
}
}
}
return null;
}
/**
* Parse throttling operations from function code
*/
parseThrottlingOperations(functionCode) {
const operations = [];
try {
// Look for reverse operations
if (functionCode.includes('.reverse()')) {
operations.push({ type: 'reverse', params: [] });
}
// Look for splice operations
const spliceMatches = functionCode.matchAll(/\.splice\(\s*(\d+)\s*,\s*(\d+)\s*\)/g);
for (const match of spliceMatches) {
operations.push({
type: 'splice',
params: [parseInt(match[1]), parseInt(match[2])]
});
}
// Look for swap operations
const swapMatches = functionCode.matchAll(/\[(\d+)\]\s*=\s*\[(\d+)\]/g);
for (const match of swapMatches) {
operations.push({
type: 'swap',
params: [parseInt(match[1]), parseInt(match[2])]
});
}
// Look for complex transformations
if (functionCode.includes('split') && functionCode.includes('join')) {
operations.push({ type: 'transform', params: [] });
}
}
catch (error) {
console.error(`[ThrottlingDecryptor] Error parsing operations: ${error.message}`);
}
return operations;
}
/**
* Apply throttling operations to decrypt parameter
*/
applyThrottlingOperations(nParam, operations) {
try {
let result = nParam.split('');
for (const operation of operations) {
switch (operation.type) {
case 'reverse':
result = result.reverse();
break;
case 'splice':
if (operation.params.length >= 2) {
const [start, deleteCount] = operation.params;
result.splice(start, deleteCount);
}
break;
case 'swap':
if (operation.params.length >= 2) {
const [i, j] = operation.params;
if (i < result.length && j < result.length) {
[result[i], result[j]] = [result[j], result[i]];
}
}
break;
case 'transform':
// Apply basic character transformation
result = this.applyCharacterTransform(result);
break;
case 'shuffle':
// Apply deterministic shuffle
result = this.applyDeterministicShuffle(result);
break;
}
}
return result.join('');
}
catch (error) {
console.error(`[ThrottlingDecryptor] Error applying operations: ${error.message}`);
return nParam; // Return original on error
}
}
/**
* Apply character transformation
*/
applyCharacterTransform(chars) {
// Basic transformation based on character codes
return chars.map(char => {
const code = char.charCodeAt(0);
if (code >= 48 && code <= 57) { // 0-9
return String.fromCharCode(((code - 48 + 5) % 10) + 48);
}
else if (code >= 65 && code <= 90) { // A-Z
return String.fromCharCode(((code - 65 + 13) % 26) + 65);
}
else if (code >= 97 && code <= 122) { // a-z
return String.fromCharCode(((code - 97 + 13) % 26) + 97);
}
return char;
});
}
/**
* Apply deterministic shuffle
*/
applyDeterministicShuffle(chars) {
const result = [...chars];
const length = result.length;
// Simple deterministic shuffle algorithm
for (let i = 0; i < length; i++) {
const j = (i * 7 + 3) % length;
[result[i], result[j]] = [result[j], result[i]];
}
return result;
}
/**
* Generate cache key from player JS
*/
generateCacheKey(playerJs) {
// Use a hash of relevant parts of the player JS
const relevantParts = playerJs.substring(0, 1000) + playerJs.substring(playerJs.length - 1000);
return this.simpleHash(relevantParts);
}
/**
* Simple hash function for cache keys
*/
simpleHash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return hash.toString(36);
}
/**
* Check if cache entry is still valid
*/
isCacheValid(entry) {
return (Date.now() - entry.timestamp) < this.CACHE_TIMEOUT;
}
/**
* Clean up expired cache entries
*/
cleanupCache() {
// const now = Date.now(); // Currently unused
for (const [key, entry] of this.cache.entries()) {
if (!this.isCacheValid(entry)) {
this.cache.delete(key);
}
}
// Limit cache size
if (this.cache.size > this.MAX_CACHE_SIZE) {
const entries = Array.from(this.cache.entries());
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
const toDelete = entries.slice(0, entries.length - this.MAX_CACHE_SIZE);
for (const [key] of toDelete) {
this.cache.delete(key);
}
}
}
/**
* Escape regex special characters
*/
escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Clear all cached data
*/
clearCache() {
this.cache.clear();
console.log('[ThrottlingDecryptor] Cache cleared');
}
/**
* Get cache statistics
*/
getCacheStats() {
return {
size: this.cache.size,
entries: Array.from(this.cache.entries()).map(([key, entry]) => ({
key,
timestamp: entry.timestamp,
success: entry.success
}))
};
}
/**
* Test throttling decryption with known parameters
*/
async testDecryption(testCases) {
const results = [];
for (const testCase of testCases) {
try {
const mockUrl = `https://example.com/video?n=${encodeURIComponent(testCase.input)}`;
const result = await this.decryptThrottlingParameter(mockUrl);
const extractedParam = this.extractNParameter(result);
results.push({
input: testCase.input,
output: extractedParam || '',
success: !testCase.expected || extractedParam === testCase.expected
});
}
catch (error) {
results.push({
input: testCase.input,
output: '',
success: false
});
}
}
return results;
}
}
exports.ThrottlingDecryptor = ThrottlingDecryptor;
// Common n-parameter function patterns found in YouTube player JS
ThrottlingDecryptor.N_FUNCTION_PATTERNS = [
// Pattern for main n-function
/([a-zA-Z_$][\w$]*)\s*=\s*function\([a-zA-Z_$][\w$]*\)\s*\{\s*[a-zA-Z_$][\w$]*\s*=\s*[a-zA-Z_$][\w$]*\.split\(\s*['"]{2}\s*\)/,
// Alternative patterns
/([a-zA-Z_$][\w$]*)\s*=\s*function\([a-zA-Z_$][\w$]*\)\s*\{\s*var\s+[a-zA-Z_$][\w$]*\s*=\s*[a-zA-Z_$][\w$]*\.split\(/,
/function\s*([a-zA-Z_$][\w$]*)\([a-zA-Z_$][\w$]*\)\s*\{\s*[a-zA-Z_$][\w$]*\s*=\s*[a-zA-Z_$][\w$]*\.split\(/,
// Compact patterns
/([a-zA-Z_$][\w$]*)\s*:\s*function\([a-zA-Z_$][\w$]*\)\s*\{\s*[a-zA-Z_$][\w$]*\s*=\s*[a-zA-Z_$][\w$]*\.split\(/
];
/**
* Singleton instance of throttling decryptor
*/
exports.throttlingDecryptor = new ThrottlingDecryptor();
//# sourceMappingURL=ThrottlingDecryptor.js.map