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