| 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;
|
|
|
|
|
| app.use(cors());
|
| app.use(express.json());
|
|
|
|
|
| app.use(express.static(__dirname));
|
|
|
|
|
| let downloadsDir;
|
|
|
|
|
| try {
|
| downloadsDir = path.join(__dirname, 'downloads');
|
| if (!fs.existsSync(downloadsDir)) {
|
| fs.mkdirSync(downloadsDir, { recursive: true });
|
| }
|
|
|
| const testFile = path.join(downloadsDir, 'test_write.tmp');
|
| fs.writeFileSync(testFile, 'test');
|
| fs.unlinkSync(testFile);
|
| } catch (error) {
|
|
|
| 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}`);
|
|
|
|
|
| function scheduleFileCleanup(filePath, delay = 5 * 60 * 1000) {
|
| setTimeout(() => {
|
| if (fs.existsSync(filePath)) {
|
| fs.unlinkSync(filePath);
|
| console.log(`🗑️ Đã xóa file: ${path.basename(filePath)}`);
|
| }
|
| }, delay);
|
| }
|
|
|
|
|
| function generateFileId() {
|
| return crypto.randomBytes(8).toString('hex');
|
| }
|
|
|
|
|
| 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' });
|
| }
|
| });
|
| });
|
|
|
|
|
| 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' });
|
| }
|
|
|
|
|
| const fileId = generateFileId();
|
| const timestamp = Date.now();
|
|
|
| let command;
|
| let expectedExtension;
|
|
|
| if (format === '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 {
|
|
|
| expectedExtension = 'mp4';
|
| const outputTemplate = path.join(downloadsDir, `${fileId}.%(ext)s`);
|
|
|
|
|
| let formatFlag = '';
|
| if (quality === 'best') {
|
| formatFlag = '';
|
| } 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);
|
|
|
|
|
| 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);
|
|
|
|
|
| scheduleFileCleanup(filePath);
|
|
|
|
|
| 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' });
|
| }
|
| });
|
| });
|
|
|
|
|
| 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' });
|
| }
|
| });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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' });
|
| }
|
| });
|
|
|
|
|
| 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()
|
| });
|
| }
|
| });
|
| });
|
|
|
|
|
| app.get('/', (req, res) => {
|
| res.redirect('/demo.html');
|
| });
|
|
|
|
|
| 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}`);
|
| });
|
|
|