Spaces:
Sleeping
Sleeping
| 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}`); | |
| }); | |