- Remove old backend (transcription, translation, summarization) - Add Camoufox stealth cookie extraction - Add automatic cookie refresh (14 days) - Add cookie validation - Simplified to focus on YouTube → MP3 downloads - Auto-retry on bot detection - Streaming support with range requests - Clean architecture (services pattern) - Full documentation
189 lines
5.1 KiB
JavaScript
189 lines
5.1 KiB
JavaScript
const { exec } = require('child_process');
|
|
const { promisify } = require('util');
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
|
|
const execAsync = promisify(exec);
|
|
|
|
/**
|
|
* Manages YouTube cookies lifecycle using Camoufox stealth extraction.
|
|
* Auto-refresh when expired, validates periodically.
|
|
*/
|
|
class CookiesManager {
|
|
constructor() {
|
|
this.cookiesPath = path.join(__dirname, '../../youtube-cookies.txt');
|
|
this.pythonPath = process.env.PYTHON_PATH || 'python3';
|
|
this.extractScript = path.join(__dirname, '../python/extract_cookies.py');
|
|
this.validateScript = path.join(__dirname, '../python/validate_cookies.py');
|
|
|
|
this.lastRefresh = null;
|
|
this.isValid = false;
|
|
|
|
// Refresh cookies every 14 days (YouTube cookies typically last 2-4 weeks)
|
|
this.refreshIntervalDays = 14;
|
|
|
|
// Check interval (every 12 hours)
|
|
this.checkIntervalMs = 12 * 60 * 60 * 1000;
|
|
}
|
|
|
|
/**
|
|
* Initialize cookies manager.
|
|
* Check if cookies exist, validate them, refresh if needed.
|
|
*/
|
|
async init() {
|
|
console.log('🔧 Initializing cookies manager...');
|
|
|
|
// Check if cookies file exists
|
|
try {
|
|
await fs.access(this.cookiesPath);
|
|
console.log('✅ Cookies file exists');
|
|
|
|
// Validate cookies
|
|
const valid = await this.validate();
|
|
if (!valid) {
|
|
console.log('⚠️ Cookies invalid, refreshing...');
|
|
await this.refresh();
|
|
} else {
|
|
console.log('✅ Cookies valid');
|
|
}
|
|
} catch (err) {
|
|
console.log('📝 No cookies found, generating fresh cookies...');
|
|
await this.refresh();
|
|
}
|
|
|
|
// Setup periodic validation (every 12 hours)
|
|
setInterval(() => {
|
|
this.checkAndRefresh().catch(err => {
|
|
console.error('Auto-check failed:', err.message);
|
|
});
|
|
}, this.checkIntervalMs);
|
|
|
|
console.log('✅ Cookies manager ready');
|
|
}
|
|
|
|
/**
|
|
* Validate cookies using Python script.
|
|
* @returns {Promise<boolean>} True if cookies are valid
|
|
*/
|
|
async validate() {
|
|
try {
|
|
const { stdout, stderr } = await execAsync(
|
|
`${this.pythonPath} ${this.validateScript} ${this.cookiesPath}`,
|
|
{ timeout: 60000 }
|
|
);
|
|
|
|
// Check for validation success in output
|
|
this.isValid = stdout.includes('Cookies are valid');
|
|
|
|
if (stderr && !stderr.includes('DeprecationWarning')) {
|
|
console.warn('Validation stderr:', stderr.trim());
|
|
}
|
|
|
|
return this.isValid;
|
|
} catch (err) {
|
|
console.error('Validation failed:', err.message);
|
|
this.isValid = false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refresh cookies using Camoufox extraction.
|
|
* @returns {Promise<boolean>} True if refresh succeeded
|
|
*/
|
|
async refresh() {
|
|
console.log('🔄 Refreshing YouTube cookies with Camoufox...');
|
|
|
|
try {
|
|
const { stdout, stderr } = await execAsync(
|
|
`${this.pythonPath} ${this.extractScript} ${this.cookiesPath}`,
|
|
{ timeout: 120000 } // 2 min timeout
|
|
);
|
|
|
|
console.log(stdout.trim());
|
|
|
|
if (stderr && !stderr.includes('DeprecationWarning')) {
|
|
console.warn('Camoufox stderr:', stderr.trim());
|
|
}
|
|
|
|
// Verify file was created
|
|
try {
|
|
await fs.access(this.cookiesPath);
|
|
this.lastRefresh = Date.now();
|
|
this.isValid = true;
|
|
console.log('✅ Cookies refreshed successfully');
|
|
return true;
|
|
} catch {
|
|
console.error('❌ Cookies file not created');
|
|
this.isValid = false;
|
|
return false;
|
|
}
|
|
} catch (err) {
|
|
console.error('❌ Failed to refresh cookies:', err.message);
|
|
this.isValid = false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check cookies age and validity, refresh if needed.
|
|
*/
|
|
async checkAndRefresh() {
|
|
console.log('🔍 Checking cookies status...');
|
|
|
|
// Check file age
|
|
try {
|
|
const stats = await fs.stat(this.cookiesPath);
|
|
const ageMs = Date.now() - stats.mtimeMs;
|
|
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
|
|
|
console.log(` Age: ${ageDays.toFixed(1)} days`);
|
|
|
|
// Refresh if too old
|
|
if (ageDays >= this.refreshIntervalDays) {
|
|
console.log(` Age threshold (${this.refreshIntervalDays} days) reached, refreshing...`);
|
|
await this.refresh();
|
|
return;
|
|
}
|
|
} catch {
|
|
// File doesn't exist
|
|
console.log(' Cookies file missing, refreshing...');
|
|
await this.refresh();
|
|
return;
|
|
}
|
|
|
|
// Validate cookies
|
|
const valid = await this.validate();
|
|
if (!valid) {
|
|
console.log(' Cookies invalid, refreshing...');
|
|
await this.refresh();
|
|
} else {
|
|
console.log(' Cookies OK ✅');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get path to cookies file.
|
|
* @returns {string} Cookies file path
|
|
*/
|
|
getCookiesPath() {
|
|
return this.cookiesPath;
|
|
}
|
|
|
|
/**
|
|
* Get cookies status.
|
|
* @returns {object} Status object
|
|
*/
|
|
getStatus() {
|
|
return {
|
|
valid: this.isValid,
|
|
path: this.cookiesPath,
|
|
lastRefresh: this.lastRefresh,
|
|
refreshIntervalDays: this.refreshIntervalDays
|
|
};
|
|
}
|
|
}
|
|
|
|
// Export singleton
|
|
module.exports = new CookiesManager();
|