This commit is contained in:
Andrey Kondratev
2025-08-27 18:57:09 +05:00
parent 98787a382e
commit 57f0519a32
13 changed files with 829 additions and 550 deletions

199
src/server.ts Normal file
View File

@@ -0,0 +1,199 @@
import express, { Request, Response, NextFunction } from 'express';
import path from 'path';
import fs from 'fs';
import ffmpeg from 'fluent-ffmpeg';
import { Database } from './database';
import { SoundCloudService } from './soundcloud';
const app = express();
const port = process.env.PORT || 3000;
// Initialize services
const db = new Database();
const soundcloud = new SoundCloudService();
// Middleware
app.use(express.json());
app.use(express.static('public'));
// Ensure downloads directory exists
const downloadsDir = path.join(__dirname, '../downloads');
if (!fs.existsSync(downloadsDir)) {
fs.mkdirSync(downloadsDir, { recursive: true });
}
// Routes
app.get('/', (req: Request, res: Response) => {
res.sendFile(path.join(__dirname, '../public/index.html'));
});
// Search videos
app.post('/api/search', async (req: Request, res: Response) => {
try {
const { query, userId }: { query?: string; userId?: string } = req.body;
if (!query || query.trim().length === 0) {
return res.status(400).json({ error: 'Query is required' });
}
// Save search history
if (userId && userId !== 'demo') {
try {
const user = await db.getUserByTelegramId(userId);
if (user) {
await db.addSearchHistory(user.id, query);
}
} catch (dbError) {
console.error('Database error:', dbError);
}
}
const videos = await soundcloud.searchTracks(query.trim());
res.json({ videos });
} catch (error) {
console.error('Search error:', error);
res.status(500).json({ error: 'Failed to search videos' });
}
});
// Convert video to MP3
app.post('/api/convert', async (req: Request, res: Response) => {
try {
const { videoId, title, userId }: { videoId?: string; title?: string; userId?: string } = req.body;
console.log('Convert request received:', { videoId, title, userId });
if (!videoId) {
return res.status(400).json({ error: 'Video ID is required' });
}
// Generate safe filename
const safeTitle = (title || '').replace(/[^\w\s-]/g, '').replace(/\s+/g, '_').substring(0, 50);
const filename = `${videoId}_${safeTitle}.mp3`;
const outputPath = path.join(downloadsDir, filename);
// Check if file already exists
if (fs.existsSync(outputPath)) {
console.log('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}`);
try {
// Get audio stream from YouTube
console.log(`Attempting to get audio stream for: ${videoId}`);
const audioStream = await soundcloud.getAudioStream(videoId);
console.log('Audio stream obtained, starting FFmpeg conversion...');
// Convert to MP3 using ffmpeg
await new Promise<void>((resolve, reject) => {
const conversion = ffmpeg(audioStream)
.audioCodec('libmp3lame')
.audioBitrate('192k')
.audioChannels(2)
.audioFrequency(44100)
.format('mp3')
.output(outputPath)
.on('start', (command: string) => {
console.log('FFmpeg started:', command);
})
.on('progress', (progress: any) => {
if (progress.percent) {
console.log(`Conversion progress: ${Math.round(progress.percent)}%`);
}
})
.on('end', () => {
console.log('MP3 conversion completed successfully');
resolve();
})
.on('error', (err: Error) => {
console.error('FFmpeg error:', err.message);
reject(err);
});
conversion.run();
});
// Save download record
if (userId && userId !== 'demo') {
try {
const user = await db.getUserByTelegramId(userId);
if (user) {
await db.addDownload(user.id, videoId, title || '', outputPath);
}
} catch (dbError) {
console.error('Database error:', dbError);
}
}
const audioUrl = `${req.protocol}://${req.get('host')}/downloads/${filename}`;
console.log('Conversion successful, file available at:', 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);
// Return error - no fallbacks for Telegram bot
return res.status(503).json({
error: 'MP3 conversion failed. This video may be restricted or unavailable for download.',
details: conversionError.message,
videoId: videoId
});
}
} catch (error) {
console.error('Server error:', error);
res.status(500).json({ error: 'Failed to process request' });
}
});
// Serve download files
app.use('/downloads', express.static(downloadsDir));
// Health check
app.get('/health', (req: Request, res: Response) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Error handler
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).json({ error: 'Something went wrong!' });
});
// Cleanup old files periodically (every hour)
setInterval(() => {
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
const now = Date.now();
fs.readdir(downloadsDir, (err, files) => {
if (err) return;
files.forEach(file => {
const filePath = path.join(downloadsDir, file);
fs.stat(filePath, (err, stats) => {
if (err) return;
if (now - stats.mtime.getTime() > maxAge) {
fs.unlink(filePath, (err) => {
if (!err) {
console.log('Deleted old file:', file);
}
});
}
});
});
});
}, 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}`);
});
export default app;