ts
This commit is contained in:
199
src/server.ts
Normal file
199
src/server.ts
Normal 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;
|
||||
Reference in New Issue
Block a user