newpipe-extractor-js
Version:
JavaScript/Node.js port of NewPipeExtractor
378 lines • 17 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.YoutubeSignatureDecryptor = void 0;
const node_fetch_1 = __importDefault(require("node-fetch"));
// Enhanced YouTube Signature Decryptor - Based on yt-dlp and NewPipeExtractor implementations
// Handles deobfuscation of YouTube streaming URLs with comprehensive signature decryption
class YoutubeSignatureDecryptor {
constructor() {
this.playerCache = new Map();
this.signatureFunctionCache = new Map();
this.throttlingFunctionCache = new Map();
this.cachedSignatureTimestamp = null;
this.USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
}
async decryptSignature(cipher, playerResponse) {
try {
console.log('[SigDecrypt] Starting signature decryption...');
// Parse cipher parameters
const cipherParams = this.parseCipherString(cipher);
if (!cipherParams.s || !cipherParams.url || !cipherParams.sp) {
console.log('[SigDecrypt] Missing required cipher parameters');
return null;
}
console.log(`[SigDecrypt] Cipher params: sp=${cipherParams.sp}, sig_length=${cipherParams.s.length}`);
// Get player JavaScript code
const playerJsCode = await this.getPlayerJsCode(playerResponse);
if (!playerJsCode) {
console.log('[SigDecrypt] Failed to get player JS code');
return null;
}
// Extract and execute signature function
const decryptedSignature = await this.executeSignatureDecryption(cipherParams.s, playerJsCode);
if (!decryptedSignature) {
console.log('[SigDecrypt] Failed to decrypt signature');
return null;
}
// Construct final URL
const finalUrl = `${cipherParams.url}&${cipherParams.sp}=${encodeURIComponent(decryptedSignature)}`;
console.log('[SigDecrypt] Signature decryption successful');
return finalUrl;
}
catch (error) {
console.error(`[SigDecrypt] Error: ${error.message}`);
return null;
}
}
async deobfuscateSignature(obfuscatedSignature, playerResponse) {
try {
const playerJsCode = await this.getPlayerJsCode(playerResponse);
if (!playerJsCode) {
throw new Error('Could not get player JS code');
}
return await this.executeSignatureDecryption(obfuscatedSignature, playerJsCode) || obfuscatedSignature;
}
catch (error) {
console.warn('Failed to deobfuscate signature:', error);
return obfuscatedSignature;
}
}
async getSignatureTimestamp(playerResponse) {
if (this.cachedSignatureTimestamp !== null) {
return this.cachedSignatureTimestamp;
}
try {
const playerJsCode = await this.getPlayerJsCode(playerResponse);
if (!playerJsCode) {
throw new Error('Could not get player JS code');
}
const match = YoutubeSignatureDecryptor.STS_REGEX.exec(playerJsCode);
if (match && match[1]) {
this.cachedSignatureTimestamp = parseInt(match[1]);
return this.cachedSignatureTimestamp;
}
throw new Error('Could not extract signature timestamp');
}
catch (error) {
console.warn('Failed to get signature timestamp:', error.message);
return 0; // Return 0 as fallback
}
}
async deobfuscateThrottlingParameter(url, playerResponse) {
const match = YoutubeSignatureDecryptor.THROTTLING_PARAM_REGEX.exec(url);
if (!match || !match[1]) {
return url; // No throttling parameter found
}
const obfuscatedParam = match[1];
try {
// Check cache first
const cacheKey = this.generateCacheKey(obfuscatedParam);
if (this.throttlingFunctionCache.has(cacheKey)) {
const cachedParam = this.throttlingFunctionCache.get(cacheKey);
return url.replace(`n=${obfuscatedParam}`, `n=${cachedParam}`);
}
// Get player code and attempt throttling deobfuscation
const playerJsCode = await this.getPlayerJsCode(playerResponse);
if (!playerJsCode) {
return url; // Return original URL if can't get player code
}
// For now, return original parameter as throttling deobfuscation is complex
// This could be enhanced in the future with full throttling parameter decryption
console.log('[SigDecrypt] Throttling parameter deobfuscation not fully implemented');
return url;
}
catch (error) {
console.warn('Failed to deobfuscate throttling parameter:', error.message);
return url; // Return original URL on error
}
}
// Enhanced cipher string parsing
parseCipherString(cipherStr) {
const params = {};
const pairs = cipherStr.split('&');
for (const pair of pairs) {
const [key, value] = pair.split('=');
if (key && value) {
params[key] = decodeURIComponent(value);
}
}
return params;
}
// Enhanced player JavaScript code retrieval
async getPlayerJsCode(playerResponse) {
try {
// Try to extract player URL from player response first
let playerJsUrl = null;
if (playerResponse) {
// Look for player URL in various places
const possiblePaths = [
'player.web.web.jsUrl',
'player.web.jsUrl',
'playerJsUrl',
'jsUrl'
];
for (const path of possiblePaths) {
const url = this.getNestedValue(playerResponse, path);
if (url) {
playerJsUrl = url.startsWith('//') ? `https:${url}` : url;
break;
}
}
}
// Fallback: get player URL from YouTube main page
if (!playerJsUrl) {
console.log('[SigDecrypt] Getting player URL from YouTube main page...');
playerJsUrl = await this.extractPlayerUrlFromMainPage();
}
if (!playerJsUrl) {
throw new Error('Could not find player JS URL');
}
console.log(`[SigDecrypt] Player JS URL: ${playerJsUrl.substring(0, 50)}...`);
// Check cache first
if (this.playerCache.has(playerJsUrl)) {
console.log('[SigDecrypt] Using cached player JS');
return this.playerCache.get(playerJsUrl);
}
// Download player JavaScript
const response = await (0, node_fetch_1.default)(playerJsUrl, {
headers: { 'User-Agent': this.USER_AGENT },
timeout: 30000
});
if (!response.ok) {
throw new Error(`Failed to download player code: ${response.status}`);
}
const playerJs = await response.text();
// Cache the player code
this.playerCache.set(playerJsUrl, playerJs);
// Clean old cache entries (keep last 3)
if (this.playerCache.size > 3) {
const firstKey = this.playerCache.keys().next().value;
if (firstKey) {
this.playerCache.delete(firstKey);
}
}
console.log(`[SigDecrypt] Downloaded player JS: ${playerJs.length} characters`);
return playerJs;
}
catch (error) {
console.error(`[SigDecrypt] Failed to get player JS: ${error.message}`);
return null;
}
}
// Enhanced player URL extraction from YouTube main page
async extractPlayerUrlFromMainPage() {
try {
const response = await (0, node_fetch_1.default)('https://www.youtube.com/', {
headers: { 'User-Agent': this.USER_AGENT },
timeout: 15000
});
if (!response.ok) {
throw new Error(`Failed to fetch YouTube main page: ${response.status}`);
}
const pageContent = await response.text();
// Multiple patterns to find player URL
const patterns = [
/"jsUrl":"([^"]+)"/,
/ytplayer\.config\s*=\s*.*?"jsUrl"\s*:\s*"([^"]+)"/,
/"WEB_PLAYER_CONTEXT_CONFIG_ID_KEVLAR_WATCH":"[^"]*","PLAYER_JS_URL":"([^"]+)"/
];
for (const pattern of patterns) {
const match = pageContent.match(pattern);
if (match && match[1]) {
let url = match[1].replace(/\\u0026/g, '&').replace(/\\/g, '');
if (url.startsWith('//')) {
url = 'https:' + url;
}
else if (url.startsWith('/')) {
url = 'https://www.youtube.com' + url;
}
return url;
}
}
throw new Error('Player URL not found in main page');
}
catch (error) {
console.error(`[SigDecrypt] Failed to extract player URL: ${error.message}`);
return null;
}
}
// Enhanced signature decryption execution
async executeSignatureDecryption(encryptedSignature, playerJsCode) {
try {
// Check cache first
const cacheKey = this.generateCacheKey(playerJsCode);
if (this.signatureFunctionCache.has(cacheKey)) {
const cachedFunction = this.signatureFunctionCache.get(cacheKey);
return this.applySignatureTransforms(encryptedSignature, cachedFunction);
}
console.log('[SigDecrypt] Extracting signature function from player JS...');
// Extract signature function name
const functionName = this.extractSignatureFunctionName(playerJsCode);
if (!functionName) {
throw new Error('Could not find signature function name');
}
console.log(`[SigDecrypt] Found signature function: ${functionName}`);
// Extract transformation plan
const transformPlan = this.extractTransformationPlan(functionName, playerJsCode);
if (!transformPlan || transformPlan.length === 0) {
throw new Error('Could not extract transformation plan');
}
console.log(`[SigDecrypt] Found ${transformPlan.length} transformation operations`);
// Cache the transformation plan
this.signatureFunctionCache.set(cacheKey, transformPlan);
// Apply transformations
return this.applySignatureTransforms(encryptedSignature, transformPlan);
}
catch (error) {
console.error(`[SigDecrypt] Signature function extraction failed: ${error.message}`);
return null;
}
}
// Enhanced signature function name extraction
extractSignatureFunctionName(playerJs) {
for (const regex of YoutubeSignatureDecryptor.FUNCTION_REGEXES) {
const match = regex.exec(playerJs);
if (match && match[1]) {
return match[1];
}
}
return undefined;
}
// Enhanced transformation plan extraction
extractTransformationPlan(functionName, playerJs) {
try {
// Find the function definition
const escapedName = functionName.replace(/\$/g, '\\$');
const functionPattern = new RegExp(escapedName + '\\s*=\\s*function\\s*\\([^)]*\\)\\s*\\{([^}]+)\\}');
const functionMatch = playerJs.match(functionPattern);
if (!functionMatch) {
throw new Error(`Function ${functionName} not found`);
}
const functionBody = functionMatch[1];
// Extract transformation statements
const statements = functionBody.split(';').map(s => s.trim()).filter(s => s);
const transformations = [];
for (const statement of statements) {
// Parse different transformation types
if (statement.includes('.reverse()')) {
transformations.push({ type: 'reverse' });
}
else if (statement.includes('.splice(')) {
const spliceMatch = statement.match(/\.splice\(\s*(\d+)\s*\)/);
if (spliceMatch) {
transformations.push({
type: 'splice',
parameter: parseInt(spliceMatch[1])
});
}
}
else {
// Look for swap operations
const swapMatch = statement.match(/\[(\d+)\].*?\[.*?(\d+).*?\]/);
if (swapMatch) {
transformations.push({
type: 'swap',
parameter: parseInt(swapMatch[2])
});
}
}
}
return transformations;
}
catch (error) {
console.error(`[SigDecrypt] Transform plan extraction failed: ${error.message}`);
return null;
}
}
// Enhanced signature transformation application
applySignatureTransforms(signature, transformPlan) {
try {
let sigArray = signature.split('');
for (const transform of transformPlan) {
switch (transform.type) {
case 'reverse':
sigArray.reverse();
break;
case 'splice':
sigArray = sigArray.slice(transform.parameter);
break;
case 'swap':
if (sigArray.length > transform.parameter) {
const temp = sigArray[0];
sigArray[0] = sigArray[transform.parameter % sigArray.length];
sigArray[transform.parameter % sigArray.length] = temp;
}
break;
}
}
const result = sigArray.join('');
console.log(`[SigDecrypt] Applied ${transformPlan.length} transformations`);
return result;
}
catch (error) {
console.error(`[SigDecrypt] Transform application failed: ${error.message}`);
return signature; // Return original on failure
}
}
// Helper method to get nested object values
getNestedValue(obj, path) {
return path.split('.').reduce((current, key) => current && current[key], obj);
}
// Generate cache key from string
generateCacheKey(input) {
// Simple hash function for caching
let hash = 0;
for (let i = 0; i < Math.min(input.length, 1000); i++) {
const char = input.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return hash.toString();
}
// Clear all caches
clearCache() {
this.playerCache.clear();
this.signatureFunctionCache.clear();
this.throttlingFunctionCache.clear();
this.cachedSignatureTimestamp = null;
console.log('[SigDecrypt] Cache cleared');
}
}
exports.YoutubeSignatureDecryptor = YoutubeSignatureDecryptor;
// Enhanced regex patterns for finding deobfuscation function (based on yt-dlp)
YoutubeSignatureDecryptor.FUNCTION_REGEXES = [
/\bm=([a-zA-Z0-9$]{2,})\(decodeURIComponent\(h\.s\)\)/,
/\bc&&\(c=([a-zA-Z0-9$]{2,})\(decodeURIComponent\(c\)\)/,
/(?:\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*\)/,
/([\w$]+)\s*=\s*function\((\w+)\)\{\s*\2=\s*\2\.split\(""\)\s*;/,
/\.sig\|\|([a-zA-Z0-9$]+)\(/,
/yt\.akamaized\.net\/\)\s*\|\|\s*.*?\s*c\s*&&\s*d\.set\([^,]+\s*,\s*(?:encodeURIComponent\s*\()?\s*([a-zA-Z0-9$]+)\(/,
/\bc\s*&&\s*d\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*([a-zA-Z0-9$]+)\(/,
/\bc\s*&&\s*d\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*([a-zA-Z0-9$]+)\(/
];
YoutubeSignatureDecryptor.STS_REGEX = /signatureTimestamp[=:](\d+)/;
YoutubeSignatureDecryptor.THROTTLING_PARAM_REGEX = /[&?]n=([^&]+)/;
//# sourceMappingURL=YoutubeSignatureDecryptor.js.map