const express = require('express'); const cors = require('cors'); const { exec } = require('child_process'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const os = require('os'); const app = express(); const PORT = process.env.PORT || 7860; // Hugging Face Spaces sử dụng port 7860 // Middleware app.use(cors()); app.use(express.json()); // Serve static files (for demo.html) app.use(express.static(__dirname)); // Tạo thư mục downloads nếu chưa tồn tại với fallback cho container let downloadsDir; // Thử tạo thư mục downloads trong app directory try { downloadsDir = path.join(__dirname, 'downloads'); if (!fs.existsSync(downloadsDir)) { fs.mkdirSync(downloadsDir, { recursive: true }); } // Test write permission const testFile = path.join(downloadsDir, 'test_write.tmp'); fs.writeFileSync(testFile, 'test'); fs.unlinkSync(testFile); } catch (error) { // Nếu không thể tạo trong app dir, dùng temp directory console.log('⚠️ Không thể ghi vào thư mục app, sử dụng temp directory'); downloadsDir = path.join(os.tmpdir(), 'ytdlp_downloads'); if (!fs.existsSync(downloadsDir)) { fs.mkdirSync(downloadsDir, { recursive: true }); } } console.log(`📁 Thư mục downloads: ${downloadsDir}`); // Auto-cleanup function - xóa file sau 5 phút function scheduleFileCleanup(filePath, delay = 5 * 60 * 1000) { // 5 phút setTimeout(() => { if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); console.log(`🗑️ Đã xóa file: ${path.basename(filePath)}`); } }, delay); } // Generate unique ID for files function generateFileId() { return crypto.randomBytes(8).toString('hex'); } // Route để lấy thông tin video app.post('/video-info', async (req, res) => { const { url } = req.body; if (!url) { return res.status(400).json({ error: 'URL là bắt buộc' }); } const command = `yt-dlp --dump-json "${url}"`; exec(command, (error, stdout, stderr) => { if (error) { console.error('Lỗi:', error); return res.status(500).json({ error: 'Không thể lấy thông tin video', details: stderr }); } try { const videoInfo = JSON.parse(stdout); res.json({ title: videoInfo.title, duration: videoInfo.duration, uploader: videoInfo.uploader, view_count: videoInfo.view_count, thumbnail: videoInfo.thumbnail, formats: videoInfo.formats.map(format => ({ format_id: format.format_id, ext: format.ext, resolution: format.resolution, filesize: format.filesize, quality: format.quality })) }); } catch (parseError) { res.status(500).json({ error: 'Lỗi phân tích dữ liệu video' }); } }); }); // Route để tải video app.post('/download', async (req, res) => { const { url, format = 'video', quality = 'best' } = req.body; if (!url) { return res.status(400).json({ error: 'URL là bắt buộc' }); } // Tạo ID duy nhất cho file const fileId = generateFileId(); const timestamp = Date.now(); let command; let expectedExtension; if (format === 'audio') { // Tải audio expectedExtension = 'mp3'; const outputTemplate = path.join(downloadsDir, `${fileId}.%(ext)s`); command = `yt-dlp -x --audio-format mp3 --audio-quality ${quality} -o "${outputTemplate}" "${url}"`; } else { // Tải video với format selection tối ưu expectedExtension = 'mp4'; const outputTemplate = path.join(downloadsDir, `${fileId}.%(ext)s`); // Xử lý format selection để tránh warning let formatFlag = ''; if (quality === 'best') { formatFlag = ''; // Không cần -f flag, để yt-dlp tự chọn best } else if (quality === 'worst') { formatFlag = '-f "worst"'; } else { formatFlag = `-f "bestvideo[height<=${quality.replace('p', '')}]+bestaudio/best[height<=${quality.replace('p', '')}]"`; } command = `yt-dlp ${formatFlag} -o "${outputTemplate}" "${url}"`.replace(/\s+/g, ' ').trim(); } console.log('Đang thực thi lệnh:', command); exec(command, (error, stdout, stderr) => { if (error) { console.error('Lỗi tải xuống:', error); return res.status(500).json({ error: 'Không thể tải video', details: stderr }); } console.log('Kết quả:', stdout); // Tìm file đã tải với fileId const files = fs.readdirSync(downloadsDir).filter(file => file.startsWith(fileId) ); if (files.length > 0) { const downloadedFile = files[0]; const filePath = path.join(downloadsDir, downloadedFile); const stats = fs.statSync(filePath); // Lên lịch xóa file sau 5 phút scheduleFileCleanup(filePath); // Lấy thông tin video để trả về const getVideoInfoCommand = `yt-dlp --dump-json --no-download "${url}"`; exec(getVideoInfoCommand, (infoError, infoStdout) => { let videoInfo = null; if (!infoError) { try { videoInfo = JSON.parse(infoStdout); } catch (e) { console.log('Không thể parse thông tin video'); } } res.json({ success: true, message: 'Tải video thành công', fileId: fileId, filename: downloadedFile, originalTitle: videoInfo?.title || 'Unknown', size: stats.size, format: format, quality: quality, duration: videoInfo?.duration || null, thumbnail: videoInfo?.thumbnail || null, uploader: videoInfo?.uploader || null, download_url: `/download-file/${downloadedFile}`, direct_link: `${req.protocol}://${req.get('host')}/download-file/${downloadedFile}`, expires_in: '5 phút', created_at: new Date().toISOString() }); }); } else { res.status(500).json({ error: 'Không tìm thấy file đã tải' }); } }); }); // Route để tải file đã download app.get('/download-file/:filename', (req, res) => { const { filename } = req.params; const filePath = path.join(downloadsDir, filename); if (fs.existsSync(filePath)) { res.download(filePath); } else { res.status(404).json({ error: 'File không tồn tại' }); } }); // Route để liệt kê các file đã tải (bỏ route này) // app.get('/downloads', (req, res) => { // const files = fs.readdirSync(downloadsDir).map(filename => { // const filePath = path.join(downloadsDir, filename); // const stats = fs.statSync(filePath); // return { // filename, // size: stats.size, // created: stats.birthtime, // download_url: `/download-file/${filename}` // }; // }); // // res.json(files); // }); // Route để xóa file app.delete('/delete/:filename', (req, res) => { const { filename } = req.params; const filePath = path.join(downloadsDir, filename); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); res.json({ success: true, message: 'Đã xóa file thành công' }); } else { res.status(404).json({ error: 'File không tồn tại' }); } }); // Route kiểm tra trạng thái app.get('/status', (req, res) => { exec('yt-dlp --version', (error, stdout, stderr) => { if (error) { res.json({ status: 'error', message: 'yt-dlp không được cài đặt hoặc không hoạt động', error: error.message }); } else { res.json({ status: 'ok', message: 'API hoạt động bình thường', yt_dlp_version: stdout.trim() }); } }); }); // Route mặc định - redirect to demo app.get('/', (req, res) => { res.redirect('/demo.html'); }); // API info route app.get('/api', (req, res) => { res.json({ message: 'YouTube Downloader API', version: '2.0.0', endpoints: { 'GET /status': 'Kiểm tra trạng thái API', 'POST /video-info': 'Lấy thông tin video (body: {url})', 'POST /download': 'Tải video (body: {url, format?, quality?})', 'GET /downloads': 'Liệt kê các file đã tải', 'GET /download-file/:filename': 'Tải file đã download', 'DELETE /delete/:filename': 'Xóa file' }, demo: 'http://localhost:' + PORT + '/demo.html' }); }); app.listen(PORT, '0.0.0.0', () => { console.log(`🚀 YouTube Downloader API đang chạy tại http://localhost:${PORT}`); console.log(`📱 Giao diện web: http://localhost:${PORT}`); console.log(`📁 Thư mục tải xuống: ${downloadsDir}`); });