Spaces:
Sleeping
Sleeping
Upload 8 files
Browse files- DEPLOY_GUIDE.md +25 -0
- Dockerfile +8 -2
- app.js +53 -10
- config.json +14 -1
- proxy-config.js +91 -0
DEPLOY_GUIDE.md
CHANGED
|
@@ -62,10 +62,14 @@ git push
|
|
| 62 |
- Install yt-dlp qua pip
|
| 63 |
- Port 7860 (mặc định của HF)
|
| 64 |
- Bind `0.0.0.0` để accessible
|
|
|
|
|
|
|
| 65 |
|
| 66 |
### App.js changes:
|
| 67 |
- Port: `process.env.PORT || 7860`
|
| 68 |
- Listen: `app.listen(PORT, '0.0.0.0')`
|
|
|
|
|
|
|
| 69 |
|
| 70 |
### Demo.html changes:
|
| 71 |
- API_BASE: `window.location.origin` (tự động)
|
|
@@ -89,3 +93,24 @@ https://huggingface.co/spaces/[YOUR_USERNAME]/ytdlp-web
|
|
| 89 |
- **App không start**: Kiểm tra port 7860
|
| 90 |
- **API không hoạt động**: Kiểm tra CORS và API_BASE
|
| 91 |
- **No space left**: Bật auto-cleanup trong config.json
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
- Install yt-dlp qua pip
|
| 63 |
- Port 7860 (mặc định của HF)
|
| 64 |
- Bind `0.0.0.0` để accessible
|
| 65 |
+
- **Fix permission**: Tạo user và set chmod 777 cho downloads/
|
| 66 |
+
- **Fallback storage**: Dùng temp directory nếu app dir không writable
|
| 67 |
|
| 68 |
### App.js changes:
|
| 69 |
- Port: `process.env.PORT || 7860`
|
| 70 |
- Listen: `app.listen(PORT, '0.0.0.0')`
|
| 71 |
+
- **Permission handling**: Auto fallback to temp directory
|
| 72 |
+
- **Format selection**: Tối ưu để tránh yt-dlp warnings
|
| 73 |
|
| 74 |
### Demo.html changes:
|
| 75 |
- API_BASE: `window.location.origin` (tự động)
|
|
|
|
| 93 |
- **App không start**: Kiểm tra port 7860
|
| 94 |
- **API không hoạt động**: Kiểm tra CORS và API_BASE
|
| 95 |
- **No space left**: Bật auto-cleanup trong config.json
|
| 96 |
+
- **Permission denied**: App tự động fallback sang temp directory
|
| 97 |
+
- **yt-dlp warnings**: Đã tối ưu format selection
|
| 98 |
+
- **Facebook/Instagram links**: Một số platform có thể block download
|
| 99 |
+
|
| 100 |
+
## 🔄 Common Issues & Solutions
|
| 101 |
+
|
| 102 |
+
### "Permission denied" error:
|
| 103 |
+
```
|
| 104 |
+
ERROR: unable to open for writing: [Errno 13] Permission denied
|
| 105 |
+
```
|
| 106 |
+
**Solution**: App đã được cập nhật để tự động sử dụng temp directory
|
| 107 |
+
|
| 108 |
+
### "Command failed" với yt-dlp:
|
| 109 |
+
- Kiểm tra URL có hợp lệ không
|
| 110 |
+
- Một số platform có thể thay đổi API
|
| 111 |
+
- Thử quality khác (worst thay vì best)
|
| 112 |
+
|
| 113 |
+
### App sleep trên HF Spaces:
|
| 114 |
+
- Apps miễn phí sẽ sleep sau 1 giờ không dùng
|
| 115 |
+
- Truy cập lại để wake up
|
| 116 |
+
- Upgrade Pro để avoid sleeping
|
Dockerfile
CHANGED
|
@@ -25,8 +25,14 @@ RUN npm install
|
|
| 25 |
# Copy application files
|
| 26 |
COPY . .
|
| 27 |
|
| 28 |
-
# Create downloads directory
|
| 29 |
-
RUN mkdir -p downloads
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
# Expose port 7860 (Hugging Face Spaces default)
|
| 32 |
EXPOSE 7860
|
|
|
|
| 25 |
# Copy application files
|
| 26 |
COPY . .
|
| 27 |
|
| 28 |
+
# Create downloads directory with proper permissions
|
| 29 |
+
RUN mkdir -p downloads && chmod 777 downloads
|
| 30 |
+
|
| 31 |
+
# Create user for security
|
| 32 |
+
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
|
| 33 |
+
|
| 34 |
+
# Switch to non-root user
|
| 35 |
+
USER appuser
|
| 36 |
|
| 37 |
# Expose port 7860 (Hugging Face Spaces default)
|
| 38 |
EXPOSE 7860
|
app.js
CHANGED
|
@@ -4,6 +4,8 @@ const { exec } = require('child_process');
|
|
| 4 |
const fs = require('fs');
|
| 5 |
const path = require('path');
|
| 6 |
const crypto = require('crypto');
|
|
|
|
|
|
|
| 7 |
|
| 8 |
const app = express();
|
| 9 |
const PORT = process.env.PORT || 7860; // Hugging Face Spaces sử dụng port 7860
|
|
@@ -15,12 +17,30 @@ app.use(express.json());
|
|
| 15 |
// Serve static files (for demo.html)
|
| 16 |
app.use(express.static(__dirname));
|
| 17 |
|
| 18 |
-
// Tạo thư mục downloads nếu chưa tồn tại
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
}
|
| 23 |
|
|
|
|
|
|
|
| 24 |
// Auto-cleanup function - xóa file sau 5 phút
|
| 25 |
function scheduleFileCleanup(filePath, delay = 5 * 60 * 1000) { // 5 phút
|
| 26 |
setTimeout(() => {
|
|
@@ -44,7 +64,11 @@ app.post('/video-info', async (req, res) => {
|
|
| 44 |
return res.status(400).json({ error: 'URL là bắt buộc' });
|
| 45 |
}
|
| 46 |
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
exec(command, (error, stdout, stderr) => {
|
| 50 |
if (error) {
|
|
@@ -86,6 +110,10 @@ app.post('/download', async (req, res) => {
|
|
| 86 |
const fileId = generateFileId();
|
| 87 |
const timestamp = Date.now();
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
let command;
|
| 90 |
let expectedExtension;
|
| 91 |
|
|
@@ -93,15 +121,30 @@ app.post('/download', async (req, res) => {
|
|
| 93 |
// Tải audio
|
| 94 |
expectedExtension = 'mp3';
|
| 95 |
const outputTemplate = path.join(downloadsDir, `${fileId}.%(ext)s`);
|
| 96 |
-
command = `yt-dlp -x --audio-format mp3 --audio-quality ${quality} -o "${outputTemplate}" "${url}"`;
|
| 97 |
} else {
|
| 98 |
-
// Tải video
|
| 99 |
expectedExtension = 'mp4';
|
| 100 |
const outputTemplate = path.join(downloadsDir, `${fileId}.%(ext)s`);
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
}
|
| 103 |
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
exec(command, (error, stdout, stderr) => {
|
| 107 |
if (error) {
|
|
@@ -125,7 +168,7 @@ app.post('/download', async (req, res) => {
|
|
| 125 |
scheduleFileCleanup(filePath);
|
| 126 |
|
| 127 |
// Lấy thông tin video để trả về
|
| 128 |
-
const getVideoInfoCommand = `yt-dlp --dump-json --no-download "${url}"
|
| 129 |
exec(getVideoInfoCommand, (infoError, infoStdout) => {
|
| 130 |
let videoInfo = null;
|
| 131 |
if (!infoError) {
|
|
|
|
| 4 |
const fs = require('fs');
|
| 5 |
const path = require('path');
|
| 6 |
const crypto = require('crypto');
|
| 7 |
+
const os = require('os');
|
| 8 |
+
const { getProxyForUrl, validateProxy } = require('./proxy-config');
|
| 9 |
|
| 10 |
const app = express();
|
| 11 |
const PORT = process.env.PORT || 7860; // Hugging Face Spaces sử dụng port 7860
|
|
|
|
| 17 |
// Serve static files (for demo.html)
|
| 18 |
app.use(express.static(__dirname));
|
| 19 |
|
| 20 |
+
// Tạo thư mục downloads nếu chưa tồn tại với fallback cho container
|
| 21 |
+
let downloadsDir;
|
| 22 |
+
|
| 23 |
+
// Thử tạo thư mục downloads trong app directory
|
| 24 |
+
try {
|
| 25 |
+
downloadsDir = path.join(__dirname, 'downloads');
|
| 26 |
+
if (!fs.existsSync(downloadsDir)) {
|
| 27 |
+
fs.mkdirSync(downloadsDir, { recursive: true });
|
| 28 |
+
}
|
| 29 |
+
// Test write permission
|
| 30 |
+
const testFile = path.join(downloadsDir, 'test_write.tmp');
|
| 31 |
+
fs.writeFileSync(testFile, 'test');
|
| 32 |
+
fs.unlinkSync(testFile);
|
| 33 |
+
} catch (error) {
|
| 34 |
+
// Nếu không thể tạo trong app dir, dùng temp directory
|
| 35 |
+
console.log('⚠️ Không thể ghi vào thư mục app, sử dụng temp directory');
|
| 36 |
+
downloadsDir = path.join(os.tmpdir(), 'ytdlp_downloads');
|
| 37 |
+
if (!fs.existsSync(downloadsDir)) {
|
| 38 |
+
fs.mkdirSync(downloadsDir, { recursive: true });
|
| 39 |
+
}
|
| 40 |
}
|
| 41 |
|
| 42 |
+
console.log(`📁 Thư mục downloads: ${downloadsDir}`);
|
| 43 |
+
|
| 44 |
// Auto-cleanup function - xóa file sau 5 phút
|
| 45 |
function scheduleFileCleanup(filePath, delay = 5 * 60 * 1000) { // 5 phút
|
| 46 |
setTimeout(() => {
|
|
|
|
| 64 |
return res.status(400).json({ error: 'URL là bắt buộc' });
|
| 65 |
}
|
| 66 |
|
| 67 |
+
// Lấy proxy cho URL này
|
| 68 |
+
const proxyUrl = getProxyForUrl(url);
|
| 69 |
+
const proxyFlag = proxyUrl && validateProxy(proxyUrl) ? `--proxy "${proxyUrl}"` : '';
|
| 70 |
+
|
| 71 |
+
const command = `yt-dlp ${proxyFlag} --dump-json "${url}"`.replace(/\s+/g, ' ').trim();
|
| 72 |
|
| 73 |
exec(command, (error, stdout, stderr) => {
|
| 74 |
if (error) {
|
|
|
|
| 110 |
const fileId = generateFileId();
|
| 111 |
const timestamp = Date.now();
|
| 112 |
|
| 113 |
+
// Lấy proxy cho URL này
|
| 114 |
+
const proxyUrl = getProxyForUrl(url);
|
| 115 |
+
const proxyFlag = proxyUrl && validateProxy(proxyUrl) ? `--proxy "${proxyUrl}"` : '';
|
| 116 |
+
|
| 117 |
let command;
|
| 118 |
let expectedExtension;
|
| 119 |
|
|
|
|
| 121 |
// Tải audio
|
| 122 |
expectedExtension = 'mp3';
|
| 123 |
const outputTemplate = path.join(downloadsDir, `${fileId}.%(ext)s`);
|
| 124 |
+
command = `yt-dlp ${proxyFlag} -x --audio-format mp3 --audio-quality ${quality} -o "${outputTemplate}" "${url}"`;
|
| 125 |
} else {
|
| 126 |
+
// Tải video với format selection tối ưu
|
| 127 |
expectedExtension = 'mp4';
|
| 128 |
const outputTemplate = path.join(downloadsDir, `${fileId}.%(ext)s`);
|
| 129 |
+
|
| 130 |
+
// Xử lý format selection để tránh warning
|
| 131 |
+
let formatFlag = '';
|
| 132 |
+
if (quality === 'best') {
|
| 133 |
+
formatFlag = ''; // Không cần -f flag, để yt-dlp tự chọn best
|
| 134 |
+
} else if (quality === 'worst') {
|
| 135 |
+
formatFlag = '-f "worst"';
|
| 136 |
+
} else {
|
| 137 |
+
formatFlag = `-f "bestvideo[height<=${quality.replace('p', '')}]+bestaudio/best[height<=${quality.replace('p', '')}]"`;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
command = `yt-dlp ${proxyFlag} ${formatFlag} -o "${outputTemplate}" "${url}"`.replace(/\s+/g, ' ').trim();
|
| 141 |
}
|
| 142 |
|
| 143 |
+
// Log proxy usage
|
| 144 |
+
if (proxyUrl) {
|
| 145 |
+
console.log(`🌐 Sử dụng proxy: ${proxyUrl.replace(/\/\/[^@]+@/, '//***:***@')}`);
|
| 146 |
+
}
|
| 147 |
+
console.log('Đang thực thi lệnh:', command.replace(/--proxy "[^"]+"/, '--proxy "***"'));
|
| 148 |
|
| 149 |
exec(command, (error, stdout, stderr) => {
|
| 150 |
if (error) {
|
|
|
|
| 168 |
scheduleFileCleanup(filePath);
|
| 169 |
|
| 170 |
// Lấy thông tin video để trả về
|
| 171 |
+
const getVideoInfoCommand = `yt-dlp ${proxyFlag} --dump-json --no-download "${url}"`.replace(/\s+/g, ' ').trim();
|
| 172 |
exec(getVideoInfoCommand, (infoError, infoStdout) => {
|
| 173 |
let videoInfo = null;
|
| 174 |
if (!infoError) {
|
config.json
CHANGED
|
@@ -25,6 +25,19 @@
|
|
| 25 |
"audioPlayer": true,
|
| 26 |
"directDownload": true,
|
| 27 |
"copyLink": true,
|
| 28 |
-
"fileIdSystem": true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
}
|
| 30 |
}
|
|
|
|
| 25 |
"audioPlayer": true,
|
| 26 |
"directDownload": true,
|
| 27 |
"copyLink": true,
|
| 28 |
+
"fileIdSystem": true,
|
| 29 |
+
"proxySupport": true
|
| 30 |
+
},
|
| 31 |
+
"proxy": {
|
| 32 |
+
"enabled": false,
|
| 33 |
+
"type": "socks5",
|
| 34 |
+
"host": "74.226.201.156",
|
| 35 |
+
"port": 1080,
|
| 36 |
+
"username": "dunn",
|
| 37 |
+
"password": "1234",
|
| 38 |
+
"poolEnabled": false,
|
| 39 |
+
"pool": [],
|
| 40 |
+
"platformSpecific": {},
|
| 41 |
+
"regions": {}
|
| 42 |
}
|
| 43 |
}
|
proxy-config.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Proxy Configuration for yt-dlp
|
| 2 |
+
const proxyConfig = {
|
| 3 |
+
enabled: false, // Tắt proxy theo yêu cầu
|
| 4 |
+
|
| 5 |
+
// Single proxy - Proxy mới của chủ
|
| 6 |
+
single: {
|
| 7 |
+
type: 'socks5',
|
| 8 |
+
host: '74.226.201.156',
|
| 9 |
+
port: 1080,
|
| 10 |
+
username: 'dunn',
|
| 11 |
+
password: '1234',
|
| 12 |
+
},
|
| 13 |
+
|
| 14 |
+
// Proxy pool cho load balancing - Bao gồm proxy mới
|
| 15 |
+
pool: [
|
| 16 |
+
'socks5://dunn:[email protected]:1080', // Proxy mới của chủ
|
| 17 |
+
'socks5://user1:[email protected]:1080',
|
| 18 |
+
'socks5://user2:[email protected]:1080',
|
| 19 |
+
'http://user3:[email protected]:8080',
|
| 20 |
+
// Thêm nhiều proxy khác...
|
| 21 |
+
],
|
| 22 |
+
|
| 23 |
+
// Cấu hình retry
|
| 24 |
+
retry: {
|
| 25 |
+
maxAttempts: 3,
|
| 26 |
+
rotateOnFailure: true
|
| 27 |
+
},
|
| 28 |
+
|
| 29 |
+
// Proxy cho từng platform - Sử dụng proxy mới
|
| 30 |
+
platformSpecific: {
|
| 31 |
+
'youtube.com': 'socks5://dunn:[email protected]:1080',
|
| 32 |
+
'facebook.com': 'socks5://dunn:[email protected]:1080',
|
| 33 |
+
'instagram.com': 'socks5://dunn:[email protected]:1080',
|
| 34 |
+
'tiktok.com': 'socks5://dunn:[email protected]:1080'
|
| 35 |
+
},
|
| 36 |
+
|
| 37 |
+
// Geo-targeting
|
| 38 |
+
regions: {
|
| 39 |
+
'US': ['socks5://us1.proxy.com:1080', 'socks5://us2.proxy.com:1080'],
|
| 40 |
+
'EU': ['socks5://eu1.proxy.com:1080', 'socks5://eu2.proxy.com:1080'],
|
| 41 |
+
'ASIA': ['socks5://asia1.proxy.com:1080', 'socks5://asia2.proxy.com:1080']
|
| 42 |
+
}
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
// Utility functions
|
| 46 |
+
function getProxyUrl(config) {
|
| 47 |
+
if (!config) return null;
|
| 48 |
+
|
| 49 |
+
const auth = config.username && config.password
|
| 50 |
+
? `${config.username}:${config.password}@`
|
| 51 |
+
: '';
|
| 52 |
+
|
| 53 |
+
return `${config.type}://${auth}${config.host}:${config.port}`;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
function getRandomProxy(pool) {
|
| 57 |
+
if (!pool || pool.length === 0) return null;
|
| 58 |
+
return pool[Math.floor(Math.random() * pool.length)];
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
function getProxyForUrl(url) {
|
| 62 |
+
if (!proxyConfig.enabled) return null;
|
| 63 |
+
|
| 64 |
+
// Kiểm tra platform-specific proxy
|
| 65 |
+
for (const [platform, proxy] of Object.entries(proxyConfig.platformSpecific)) {
|
| 66 |
+
if (url.includes(platform)) {
|
| 67 |
+
return proxy;
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
// Dùng proxy pool hoặc single proxy
|
| 72 |
+
if (proxyConfig.pool && proxyConfig.pool.length > 0) {
|
| 73 |
+
return getRandomProxy(proxyConfig.pool);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
return getProxyUrl(proxyConfig.single);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
function validateProxy(proxyUrl) {
|
| 80 |
+
// Basic validation
|
| 81 |
+
const proxyRegex = /^(socks5|http|https):\/\/(?:([^:]+):([^@]+)@)?([^:]+):(\d+)$/;
|
| 82 |
+
return proxyRegex.test(proxyUrl);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
module.exports = {
|
| 86 |
+
proxyConfig,
|
| 87 |
+
getProxyUrl,
|
| 88 |
+
getRandomProxy,
|
| 89 |
+
getProxyForUrl,
|
| 90 |
+
validateProxy
|
| 91 |
+
};
|