This commit is contained in:
Andrey Kondratev
2025-11-10 13:56:19 +05:00
parent 6db48b16a7
commit 82a9596370
13 changed files with 1086 additions and 152 deletions

View File

@@ -10,6 +10,7 @@ ffmpeg.setFfprobePath('/usr/bin/ffprobe');
import { Database } from './database';
import { SoundCloudService } from './soundcloud';
import { QuixoticBot } from './bot';
import { logger } from './logger';
const app = express();
const port = process.env.PORT || 3000;
@@ -70,7 +71,10 @@ if (!fs.existsSync(downloadsDir)) {
// Routes
app.get('/', (req: Request, res: Response) => {
const indexPath = path.join(__dirname, '../public/index.html');
// Use minified HTML in production
const isProduction = process.env.NODE_ENV === 'production';
const htmlFile = isProduction ? 'index.min.html' : 'index.html';
const indexPath = path.join(__dirname, '../public', htmlFile);
// Set cache headers for HTML (short cache)
res.set({
@@ -97,7 +101,7 @@ app.post('/api/search', async (req: Request, res: Response) => {
await db.addSearchHistory(user.id, query);
}
} catch (dbError) {
console.error('Database error:', dbError);
logger.error('Database error:', dbError);
}
}
@@ -105,7 +109,7 @@ app.post('/api/search', async (req: Request, res: Response) => {
res.json({ videos });
} catch (error) {
console.error('Search error:', error);
logger.error('Search error:', error);
res.status(500).json({ error: 'Failed to search videos' });
}
});
@@ -114,7 +118,7 @@ app.post('/api/search', async (req: Request, res: Response) => {
app.post('/api/convert', async (req: Request, res: Response) => {
try {
const { videoId, title, userId, url }: { videoId?: string; title?: string; userId?: string; url?: string } = req.body;
console.log('Convert request received:', { videoId, title, userId });
logger.info(`Convert request received: ${title} (ID: ${videoId})`);
if (!videoId) {
return res.status(400).json({ error: 'Video ID is required' });
@@ -127,18 +131,18 @@ app.post('/api/convert', async (req: Request, res: Response) => {
// Check if file already exists
if (fs.existsSync(outputPath)) {
console.log('File already exists, serving cached version');
logger.info('File already exists, serving cached version');
const audioUrl = `${req.protocol}://${req.get('host')}/downloads/${filename}`;
return res.json({ audioUrl, title });
}
console.log(`Starting MP3 conversion for: ${title}`);
logger.info(`Starting MP3 conversion: ${title}`);
try {
// Get audio stream from YouTube
console.log(`Attempting to get audio stream for: ${videoId}`);
logger.debug(`Attempting to get audio stream for: ${videoId}`);
const audioStream = await soundcloud.getAudioStream(videoId, url);
console.log('Audio stream obtained, starting FFmpeg conversion...');
logger.info('Audio stream obtained, starting FFmpeg conversion...');
// Download to temporary file first, then convert
const tempInputPath = path.join(downloadsDir, `temp_${videoId}.tmp`);
@@ -152,19 +156,19 @@ app.post('/api/convert', async (req: Request, res: Response) => {
writeStream.on('error', reject);
});
console.log('Temporary file saved, starting FFmpeg conversion...');
logger.info('Temporary file saved, starting FFmpeg conversion...');
// Debug: check temp file
const stats = fs.statSync(tempInputPath);
console.log(`Temp file size: ${stats.size} bytes`);
logger.debug(`Temp file size: ${stats.size} bytes`);
// Test ffmpeg with simple command first
try {
const { execSync } = require('child_process');
execSync(`ffmpeg -i "${tempInputPath}" -t 1 -f null -`, { encoding: 'utf8', stdio: 'pipe' });
console.log('FFmpeg file test passed');
logger.debug('FFmpeg file test passed');
} catch (e: any) {
console.error('FFmpeg file test failed:', e.stderr || e.message);
logger.error('FFmpeg file test failed:', e.stderr || e.message);
}
// Convert temporary file to MP3 using ffmpeg
@@ -177,23 +181,23 @@ app.post('/api/convert', async (req: Request, res: Response) => {
.format('mp3')
.output(outputPath)
.on('start', (command: string) => {
console.log('FFmpeg started:', command);
logger.ffmpeg('Started', command);
})
.on('progress', (progress: any) => {
if (progress.percent) {
console.log(`Conversion progress: ${Math.round(progress.percent)}%`);
logger.ffmpeg('Progress', `${Math.round(progress.percent)}%`);
}
})
.on('end', () => {
console.log('MP3 conversion completed successfully');
logger.success('MP3 conversion completed successfully');
// Clean up temporary file
fs.unlink(tempInputPath, (err) => {
if (err) console.error('Failed to delete temp file:', err);
if (err) logger.error('Failed to delete temp file:', err);
});
resolve();
})
.on('error', (err: Error) => {
console.error('FFmpeg error:', err.message);
logger.error('FFmpeg error:', err.message);
// Clean up temporary file on error
fs.unlink(tempInputPath, () => {});
reject(err);
@@ -210,18 +214,18 @@ app.post('/api/convert', async (req: Request, res: Response) => {
await db.addDownload(user.id, videoId, title || '', outputPath);
}
} catch (dbError) {
console.error('Database error:', dbError);
logger.error('Database error:', dbError);
}
}
const audioUrl = `${req.protocol}://${req.get('host')}/downloads/${filename}`;
console.log('Conversion successful, file available at:', audioUrl);
logger.success(`Conversion successful: ${audioUrl}`);
res.json({ audioUrl, title });
} catch (conversionError: any) {
console.error('Conversion failed for video:', videoId);
console.error('Error details:', conversionError.message);
console.error('Full error:', conversionError);
logger.error(`Conversion failed for video: ${videoId}`);
logger.error('Error details:', conversionError.message);
logger.error('Full error:', conversionError);
// Return error - no fallbacks for Telegram bot
return res.status(503).json({
@@ -232,18 +236,18 @@ app.post('/api/convert', async (req: Request, res: Response) => {
}
} catch (error) {
console.error('Server error:', error);
logger.error('Server error:', error);
res.status(500).json({ error: 'Failed to process request' });
}
});
// Direct Telegram API for sending audio
app.post('/api/telegram-send', async (req: Request, res: Response) => {
console.log('🚀 Telegram send request received');
logger.telegram('Send request received');
try {
const { userId, audioUrl, title, performer, thumbnail }: { userId?: string; audioUrl?: string; title?: string; performer?: string; thumbnail?: string } = req.body;
console.log(`📤 Sending to user ${userId}: ${title}`);
logger.telegram('Sending to user', `${userId}: ${title}`);
if (!userId || !audioUrl || !title) {
return res.status(400).json({ error: 'Missing required fields' });
@@ -251,18 +255,18 @@ app.post('/api/telegram-send', async (req: Request, res: Response) => {
const botInstance = (global as any).quixoticBot;
if (!botInstance) {
console.log('Bot not available');
logger.error('Bot not available');
return res.status(500).json({ error: 'Bot not available' });
}
const chatId = parseInt(userId);
await botInstance.sendAudioFile(chatId, audioUrl, title, performer, thumbnail);
console.log('Audio sent successfully');
logger.success('Audio sent successfully');
res.json({ success: true, message: 'Audio sent successfully' });
} catch (error: any) {
console.error('Send failed:', error.message);
logger.error('Send failed:', error.message);
res.status(500).json({ error: error.message });
}
});
@@ -277,7 +281,7 @@ app.get('/health', (req: Request, res: Response) => {
// Error handler
app.use((err: Error, _req: Request, res: Response, _next: any) => {
console.error(err.stack);
logger.error(err.stack || err.message);
res.status(500).json({ error: 'Something went wrong!' });
});
@@ -297,7 +301,7 @@ setInterval(() => {
if (now - stats.mtime.getTime() > maxAge) {
fs.unlink(filePath, (err) => {
if (!err) {
console.log('Deleted old file:', file);
logger.info('Deleted old file:', file);
}
});
}
@@ -307,9 +311,9 @@ setInterval(() => {
}, 60 * 60 * 1000); // Run every hour
app.listen(port, () => {
console.log(`Quixotic server running on port ${port}`);
console.log(`Downloads directory: ${downloadsDir}`);
console.log(`Open in browser: http://localhost:${port}`);
logger.success(`Quixotic server running on port ${port}`);
logger.info(`Downloads directory: ${downloadsDir}`);
logger.info(`Open in browser: http://localhost:${port}`);
});
// Initialize Telegram bot
@@ -321,33 +325,33 @@ if (botToken && botToken.length > 10 && botToken !== 'your_telegram_bot_token_he
const botInstance = new QuixoticBot(botToken, webAppUrl);
// Store bot instance globally for API access
(global as any).quixoticBot = botInstance;
console.log('🤖 Telegram bot started and stored globally');
logger.telegram('Bot started and stored globally');
} catch (error: any) {
console.error('Bot initialization failed:', error.message);
console.warn('⚠️ Bot disabled due to error');
console.warn('⚠️ Telegram integration will not be available');
logger.error('Bot initialization failed:', error.message);
logger.warn('Bot disabled due to error');
logger.warn('Telegram integration will not be available');
// Don't crash the server, continue without bot
}
} else {
console.warn('⚠️ TELEGRAM_BOT_TOKEN not configured properly');
console.warn('⚠️ Bot will not start - only web interface will be available');
console.warn(' To enable Telegram bot, set a valid TELEGRAM_BOT_TOKEN');
logger.warn('TELEGRAM_BOT_TOKEN not configured properly');
logger.warn('Bot will not start - only web interface will be available');
logger.info('To enable Telegram bot, set a valid TELEGRAM_BOT_TOKEN');
}
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {
console.error('🚨 Unhandled Rejection at:', promise);
console.error('Reason:', reason);
logger.error('Unhandled Rejection at:', promise);
logger.error('Reason:', reason);
// Log but don't crash the server
if (reason?.code === 'ETELEGRAM') {
console.warn('⚠️ Telegram API error - continuing operation');
logger.warn('Telegram API error - continuing operation');
}
});
// Handle uncaught exceptions
process.on('uncaughtException', (error: Error) => {
console.error('🚨 Uncaught Exception:', error);
logger.error('Uncaught Exception:', error);
// Log but try to continue
});