haaaaus commited on
Commit
b10e9c4
·
verified ·
1 Parent(s): 2be9533

Upload 10 files

Browse files
Files changed (10) hide show
  1. .gitignore +43 -0
  2. .spacesignore +7 -0
  3. DEPLOY_GUIDE.md +91 -0
  4. Dockerfile +38 -0
  5. app.js +249 -0
  6. config.json +30 -0
  7. demo-old.html +556 -0
  8. demo.html +500 -0
  9. package-lock.json +1064 -0
  10. package.json +20 -0
.gitignore ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Downloads folder
5
+ downloads/
6
+
7
+ # Environment variables
8
+ .env
9
+
10
+ # Logs
11
+ *.log
12
+ npm-debug.log*
13
+
14
+ # Runtime data
15
+ pids
16
+ *.pid
17
+ *.seed
18
+ *.pid.lock
19
+
20
+ # Directory for instrumented libs generated by jscoverage/JSCover
21
+ lib-cov
22
+
23
+ # Coverage directory used by tools like istanbul
24
+ coverage
25
+
26
+ # OS generated files
27
+ .DS_Store
28
+ .DS_Store?
29
+ ._*
30
+ .Spotlight-V100
31
+ .Trashes
32
+ ehthumbs.db
33
+ Thumbs.db
34
+
35
+ # IDE
36
+ .vscode/
37
+ .idea/
38
+ *.swp
39
+ *.swo
40
+
41
+ # Temporary files
42
+ tmp/
43
+ temp/
.spacesignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ node_modules/
2
+ downloads/
3
+ *.log
4
+ .env
5
+ .DS_Store
6
+ test.js
7
+ .git/
DEPLOY_GUIDE.md ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚀 Hướng dẫn Deploy lên Hugging Face Spaces
2
+
3
+ ## Bước 1: Chuẩn bị Repository
4
+
5
+ Đã có sẵn: https://github.com/pedguedes090/ytdlp_web
6
+
7
+ ## Bước 2: Tạo Hugging Face Space
8
+
9
+ 1. Đi tới https://huggingface.co/new-space
10
+ 2. Điền thông tin:
11
+ - **Space name**: `ytdlp-web` (hoặc tên bạn muốn)
12
+ - **License**: MIT
13
+ - **SDK**: Docker
14
+ - **Hardware**: CPU basic (miễn phí)
15
+
16
+ ## Bước 3: Clone và Setup
17
+
18
+ ```bash
19
+ git clone https://huggingface.co/spaces/[YOUR_USERNAME]/ytdlp-web
20
+ cd ytdlp-web
21
+ ```
22
+
23
+ ## Bước 4: Copy files từ GitHub repo
24
+
25
+ Copy tất cả files từ repository GitHub vào thư mục Hugging Face Space:
26
+
27
+ ```
28
+ ytdlp-web/
29
+ ├── Dockerfile # ✅ Đã tạo
30
+ ├── README.md # Thay bằng README_HF.md
31
+ ├── .spacesignore # ✅ Đã tạo
32
+ ├── app.js # ✅ Đã cập nhật port 7860
33
+ ├── demo.html # ✅ Đã cập nhật API_BASE
34
+ ├── package.json
35
+ ├── config.json
36
+ └── downloads/ # Sẽ tự tạo
37
+ ```
38
+
39
+ ## Bước 5: Cập nhật README.md
40
+
41
+ Đổi tên `README_HF.md` thành `README.md` để Hugging Face hiển thị metadata đúng.
42
+
43
+ ## Bước 6: Push lên Hugging Face
44
+
45
+ ```bash
46
+ git add .
47
+ git commit -m "Initial deploy to Hugging Face Spaces"
48
+ git push
49
+ ```
50
+
51
+ ## Bước 7: Kiểm tra Deploy
52
+
53
+ - Hugging Face sẽ tự động build Docker image
54
+ - Có thể mất 5-10 phút để build xong
55
+ - Truy cập URL của Space để kiểm tra
56
+
57
+ ## 🔧 Cấu hình quan trọng
58
+
59
+ ### Dockerfile highlights:
60
+ - Base image: `python:3.11-slim`
61
+ - Cài ffmpeg, nodejs, npm
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)
72
+
73
+ ## 🚨 Lưu ý
74
+
75
+ 1. **Miễn phí**: HF Spaces có giới hạn CPU và memory
76
+ 2. **Timeout**: Apps có thể sleep sau một thời gian không dùng
77
+ 3. **Storage**: File sẽ mất khi app restart (đã có auto-cleanup)
78
+ 4. **Public**: Space sẽ public trừ khi upgrade Pro
79
+
80
+ ## 🎯 URL sau khi deploy
81
+
82
+ ```
83
+ https://huggingface.co/spaces/[YOUR_USERNAME]/ytdlp-web
84
+ ```
85
+
86
+ ## 🛠️ Troubleshooting
87
+
88
+ - **Build failed**: Kiểm tra Dockerfile syntax
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
Dockerfile ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dockerfile for Hugging Face Spaces
2
+ FROM python:3.11-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies
8
+ RUN apt-get update && apt-get install -y \
9
+ curl \
10
+ wget \
11
+ ffmpeg \
12
+ nodejs \
13
+ npm \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
16
+ # Install yt-dlp
17
+ RUN pip install yt-dlp
18
+
19
+ # Copy package files first for better caching
20
+ COPY package*.json ./
21
+
22
+ # Install Node.js dependencies
23
+ RUN npm install
24
+
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
33
+
34
+ # Set environment variable for Hugging Face
35
+ ENV PORT=7860
36
+
37
+ # Start the application
38
+ CMD ["npm", "start"]
app.js ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const cors = require('cors');
3
+ 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
10
+
11
+ // Middleware
12
+ app.use(cors());
13
+ app.use(express.json());
14
+
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
+ const downloadsDir = path.join(__dirname, 'downloads');
20
+ if (!fs.existsSync(downloadsDir)) {
21
+ fs.mkdirSync(downloadsDir);
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(() => {
27
+ if (fs.existsSync(filePath)) {
28
+ fs.unlinkSync(filePath);
29
+ console.log(`🗑️ Đã xóa file: ${path.basename(filePath)}`);
30
+ }
31
+ }, delay);
32
+ }
33
+
34
+ // Generate unique ID for files
35
+ function generateFileId() {
36
+ return crypto.randomBytes(8).toString('hex');
37
+ }
38
+
39
+ // Route để lấy thông tin video
40
+ app.post('/video-info', async (req, res) => {
41
+ const { url } = req.body;
42
+
43
+ if (!url) {
44
+ return res.status(400).json({ error: 'URL là bắt buộc' });
45
+ }
46
+
47
+ const command = `yt-dlp --dump-json "${url}"`;
48
+
49
+ exec(command, (error, stdout, stderr) => {
50
+ if (error) {
51
+ console.error('Lỗi:', error);
52
+ return res.status(500).json({ error: 'Không thể lấy thông tin video', details: stderr });
53
+ }
54
+
55
+ try {
56
+ const videoInfo = JSON.parse(stdout);
57
+ res.json({
58
+ title: videoInfo.title,
59
+ duration: videoInfo.duration,
60
+ uploader: videoInfo.uploader,
61
+ view_count: videoInfo.view_count,
62
+ thumbnail: videoInfo.thumbnail,
63
+ formats: videoInfo.formats.map(format => ({
64
+ format_id: format.format_id,
65
+ ext: format.ext,
66
+ resolution: format.resolution,
67
+ filesize: format.filesize,
68
+ quality: format.quality
69
+ }))
70
+ });
71
+ } catch (parseError) {
72
+ res.status(500).json({ error: 'Lỗi phân tích dữ liệu video' });
73
+ }
74
+ });
75
+ });
76
+
77
+ // Route để tải video
78
+ app.post('/download', async (req, res) => {
79
+ const { url, format = 'video', quality = 'best' } = req.body;
80
+
81
+ if (!url) {
82
+ return res.status(400).json({ error: 'URL là bắt buộc' });
83
+ }
84
+
85
+ // Tạo ID duy nhất cho file
86
+ const fileId = generateFileId();
87
+ const timestamp = Date.now();
88
+
89
+ let command;
90
+ let expectedExtension;
91
+
92
+ if (format === 'audio') {
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
+ command = `yt-dlp -f "${quality}" -o "${outputTemplate}" "${url}"`;
102
+ }
103
+
104
+ console.log('Đang thực thi lệnh:', command);
105
+
106
+ exec(command, (error, stdout, stderr) => {
107
+ if (error) {
108
+ console.error('Lỗi tải xuống:', error);
109
+ return res.status(500).json({ error: 'Không thể tải video', details: stderr });
110
+ }
111
+
112
+ console.log('Kết quả:', stdout);
113
+
114
+ // Tìm file đã tải với fileId
115
+ const files = fs.readdirSync(downloadsDir).filter(file =>
116
+ file.startsWith(fileId)
117
+ );
118
+
119
+ if (files.length > 0) {
120
+ const downloadedFile = files[0];
121
+ const filePath = path.join(downloadsDir, downloadedFile);
122
+ const stats = fs.statSync(filePath);
123
+
124
+ // Lên lịch xóa file sau 5 phút
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) {
132
+ try {
133
+ videoInfo = JSON.parse(infoStdout);
134
+ } catch (e) {
135
+ console.log('Không thể parse thông tin video');
136
+ }
137
+ }
138
+
139
+ res.json({
140
+ success: true,
141
+ message: 'Tải video thành công',
142
+ fileId: fileId,
143
+ filename: downloadedFile,
144
+ originalTitle: videoInfo?.title || 'Unknown',
145
+ size: stats.size,
146
+ format: format,
147
+ quality: quality,
148
+ duration: videoInfo?.duration || null,
149
+ thumbnail: videoInfo?.thumbnail || null,
150
+ uploader: videoInfo?.uploader || null,
151
+ download_url: `/download-file/${downloadedFile}`,
152
+ direct_link: `${req.protocol}://${req.get('host')}/download-file/${downloadedFile}`,
153
+ expires_in: '5 phút',
154
+ created_at: new Date().toISOString()
155
+ });
156
+ });
157
+ } else {
158
+ res.status(500).json({ error: 'Không tìm thấy file đã tải' });
159
+ }
160
+ });
161
+ });
162
+
163
+ // Route để tải file đã download
164
+ app.get('/download-file/:filename', (req, res) => {
165
+ const { filename } = req.params;
166
+ const filePath = path.join(downloadsDir, filename);
167
+
168
+ if (fs.existsSync(filePath)) {
169
+ res.download(filePath);
170
+ } else {
171
+ res.status(404).json({ error: 'File không tồn tại' });
172
+ }
173
+ });
174
+
175
+ // Route để liệt kê các file đã tải (bỏ route này)
176
+ // app.get('/downloads', (req, res) => {
177
+ // const files = fs.readdirSync(downloadsDir).map(filename => {
178
+ // const filePath = path.join(downloadsDir, filename);
179
+ // const stats = fs.statSync(filePath);
180
+ // return {
181
+ // filename,
182
+ // size: stats.size,
183
+ // created: stats.birthtime,
184
+ // download_url: `/download-file/${filename}`
185
+ // };
186
+ // });
187
+ //
188
+ // res.json(files);
189
+ // });
190
+
191
+ // Route để xóa file
192
+ app.delete('/delete/:filename', (req, res) => {
193
+ const { filename } = req.params;
194
+ const filePath = path.join(downloadsDir, filename);
195
+
196
+ if (fs.existsSync(filePath)) {
197
+ fs.unlinkSync(filePath);
198
+ res.json({ success: true, message: 'Đã xóa file thành công' });
199
+ } else {
200
+ res.status(404).json({ error: 'File không tồn tại' });
201
+ }
202
+ });
203
+
204
+ // Route kiểm tra trạng thái
205
+ app.get('/status', (req, res) => {
206
+ exec('yt-dlp --version', (error, stdout, stderr) => {
207
+ if (error) {
208
+ res.json({
209
+ status: 'error',
210
+ message: 'yt-dlp không được cài đặt hoặc không hoạt động',
211
+ error: error.message
212
+ });
213
+ } else {
214
+ res.json({
215
+ status: 'ok',
216
+ message: 'API hoạt động bình thường',
217
+ yt_dlp_version: stdout.trim()
218
+ });
219
+ }
220
+ });
221
+ });
222
+
223
+ // Route mặc định - redirect to demo
224
+ app.get('/', (req, res) => {
225
+ res.redirect('/demo.html');
226
+ });
227
+
228
+ // API info route
229
+ app.get('/api', (req, res) => {
230
+ res.json({
231
+ message: 'YouTube Downloader API',
232
+ version: '2.0.0',
233
+ endpoints: {
234
+ 'GET /status': 'Kiểm tra trạng thái API',
235
+ 'POST /video-info': 'Lấy thông tin video (body: {url})',
236
+ 'POST /download': 'Tải video (body: {url, format?, quality?})',
237
+ 'GET /downloads': 'Liệt kê các file đã tải',
238
+ 'GET /download-file/:filename': 'Tải file đã download',
239
+ 'DELETE /delete/:filename': 'Xóa file'
240
+ },
241
+ demo: 'http://localhost:' + PORT + '/demo.html'
242
+ });
243
+ });
244
+
245
+ app.listen(PORT, '0.0.0.0', () => {
246
+ console.log(`🚀 YouTube Downloader API đang chạy tại http://localhost:${PORT}`);
247
+ console.log(`📱 Giao diện web: http://localhost:${PORT}`);
248
+ console.log(`📁 Thư mục tải xuống: ${downloadsDir}`);
249
+ });
config.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "port": 3000,
3
+ "downloadsDir": "./downloads",
4
+ "corsEnabled": true,
5
+ "defaultQuality": "worst",
6
+ "supportedFormats": ["video", "audio"],
7
+ "maxFileSize": "500MB",
8
+ "allowedOrigins": ["*"],
9
+ "app": {
10
+ "name": "YouTube Downloader API",
11
+ "version": "2.1.0",
12
+ "description": "API để tải video từ YouTube và các trang web khác sử dụng yt-dlp với video player tích hợp"
13
+ },
14
+ "limits": {
15
+ "maxConcurrentDownloads": 3,
16
+ "downloadTimeout": 300000
17
+ },
18
+ "autoCleanup": {
19
+ "enabled": true,
20
+ "fileLifetime": 300000,
21
+ "description": "File sẽ tự động xóa sau 5 phút (300000ms)"
22
+ },
23
+ "features": {
24
+ "videoPlayer": true,
25
+ "audioPlayer": true,
26
+ "directDownload": true,
27
+ "copyLink": true,
28
+ "fileIdSystem": true
29
+ }
30
+ }
demo-old.html ADDED
@@ -0,0 +1,556 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Video Downloader Online - yt-dlp API</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
10
+ body { font-family: 'Inter', sans-serif; }
11
+ .gradient-bg {
12
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
13
+ }
14
+ .glass-effect {
15
+ background: rgba(255, 255, 255, 0.1);
16
+ backdrop-filter: blur(10px);
17
+ border: 1px solid rgba(255, 255, 255, 0.2);
18
+ }
19
+ .preview-placeholder {
20
+ background: linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
21
+ linear-gradient(-45deg, #f0f0f0 25%, transparent 25%),
22
+ linear-gradient(45deg, transparent 75%, #f0f0f0 75%),
23
+ linear-gradient(-45deg, transparent 75%, #f0f0f0 75%);
24
+ background-size: 20px 20px;
25
+ background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
26
+ }
27
+ .spinner {
28
+ animation: spin 1s linear infinite;
29
+ }
30
+ @keyframes spin {
31
+ from { transform: rotate(0deg); }
32
+ to { transform: rotate(360deg); }
33
+ }
34
+ </style>
35
+ </head>
36
+ <body class="gradient-bg min-h-screen">
37
+ <!-- API Status Indicator -->
38
+ <div id="apiStatus" class="fixed top-4 right-4 z-50"></div>
39
+
40
+ <div class="container mx-auto px-4 py-8">
41
+ <!-- Header -->
42
+ <div class="text-center mb-12">
43
+ <h1 class="text-5xl font-bold text-white mb-4">
44
+ 🎬 Video Downloader
45
+ </h1>
46
+ <p class="text-xl text-white/80 max-w-2xl mx-auto">
47
+ Tải video từ YouTube và các trang web khác sử dụng yt-dlp API
48
+ </p>
49
+ </div>
50
+
51
+ <!-- Main Content -->
52
+ <div class="max-w-6xl mx-auto">
53
+ <div class="grid lg:grid-cols-2 gap-8">
54
+ <!-- Input Section -->
55
+ <div class="glass-effect rounded-2xl p-8">
56
+ <h2 class="text-2xl font-semibold text-white mb-6 flex items-center">
57
+ 📥 Nhập Liên Kết
58
+ </h2>
59
+
60
+ <div class="space-y-6">
61
+ <div>
62
+ <label class="block text-white/90 text-sm font-medium mb-3">
63
+ URL Video
64
+ </label>
65
+ <input
66
+ type="url"
67
+ id="videoUrl"
68
+ placeholder="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
69
+ value="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
70
+ class="w-full px-4 py-3 rounded-xl bg-white/10 border border-white/20 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all"
71
+ >
72
+ </div>
73
+
74
+ <div>
75
+ <label class="block text-white/90 text-sm font-medium mb-3">
76
+ Chất Lượng
77
+ </label>
78
+ <select id="qualitySelect" class="w-full px-4 py-3 rounded-xl bg-white/10 border border-white/20 text-white focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all">
79
+ <option value="best" class="bg-gray-800">Tốt nhất</option>
80
+ <option value="worst" class="bg-gray-800">Thấp nhất (nhanh hơn)</option>
81
+ <option value="720p" class="bg-gray-800">720p (HD)</option>
82
+ <option value="480p" class="bg-gray-800">480p (SD)</option>
83
+ <option value="360p" class="bg-gray-800">360p</option>
84
+ </select>
85
+ </div>
86
+
87
+ <div>
88
+ <label class="block text-white/90 text-sm font-medium mb-3">
89
+ Định Dạng
90
+ </label>
91
+ <div class="flex gap-3">
92
+ <label class="flex items-center cursor-pointer">
93
+ <input type="radio" name="format" value="video" checked class="sr-only">
94
+ <div class="w-5 h-5 rounded-full border-2 border-white/40 flex items-center justify-center mr-2">
95
+ <div class="w-2 h-2 rounded-full bg-blue-400"></div>
96
+ </div>
97
+ <span class="text-white/90">Video</span>
98
+ </label>
99
+ <label class="flex items-center cursor-pointer">
100
+ <input type="radio" name="format" value="audio" class="sr-only">
101
+ <div class="w-5 h-5 rounded-full border-2 border-white/40 flex items-center justify-center mr-2">
102
+ <div class="w-2 h-2 rounded-full bg-transparent"></div>
103
+ </div>
104
+ <span class="text-white/90">Audio MP3</span>
105
+ </label>
106
+ </div>
107
+ </div>
108
+
109
+ <div class="flex gap-3">
110
+ <button
111
+ onclick="getVideoInfo()"
112
+ class="flex-1 bg-gradient-to-r from-green-500 to-teal-600 hover:from-green-600 hover:to-teal-700 text-white font-semibold py-4 px-6 rounded-xl transition-all duration-300 transform hover:scale-105 shadow-lg"
113
+ >
114
+ 🔍 Xem Thông Tin
115
+ </button>
116
+ <button
117
+ onclick="downloadVideo()"
118
+ class="flex-1 bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white font-semibold py-4 px-6 rounded-xl transition-all duration-300 transform hover:scale-105 shadow-lg"
119
+ >
120
+ 🚀 Tải Xuống
121
+ </button>
122
+ </div>
123
+ </div>
124
+ </div>
125
+
126
+ <!-- Preview Section -->
127
+ <div class="glass-effect rounded-2xl p-8">
128
+ <h2 class="text-2xl font-semibold text-white mb-6 flex items-center">
129
+ 👁️ Thông Tin Video
130
+ </h2>
131
+
132
+ <div id="previewContainer" class="space-y-4">
133
+ <div class="preview-placeholder rounded-xl h-48 flex items-center justify-center">
134
+ <div class="text-center text-gray-500">
135
+ <div class="text-4xl mb-2">🎥</div>
136
+ <p>Nhấn "Xem Thông Tin" để hiển thị chi tiết video</p>
137
+ </div>
138
+ </div>
139
+
140
+ <div class="bg-white/5 rounded-xl p-4">
141
+ <h3 id="videoTitle" class="text-white font-medium mb-2">Tiêu đề video sẽ hiển thị ở đây</h3>
142
+ <div class="flex justify-between text-sm text-white/70">
143
+ <span id="videoDuration">Thời lượng: --:--</span>
144
+ <span id="videoViews">Lượt xem: --</span>
145
+ </div>
146
+ <div class="mt-2 text-sm text-white/70">
147
+ <span id="videoUploader">Kênh: --</span>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+
154
+ <!-- Output Section -->
155
+ <div class="glass-effect rounded-2xl p-8 mt-8">
156
+ <h2 class="text-2xl font-semibold text-white mb-6 flex items-center">
157
+ 📤 Kết Quả Tải Xuống
158
+ </h2>
159
+
160
+ <div id="outputContainer" class="space-y-4">
161
+ <div class="bg-white/5 rounded-xl p-6 text-center">
162
+ <div class="text-4xl mb-4">⏳</div>
163
+ <p class="text-white/70">Chưa có video nào được xử lý</p>
164
+ <p class="text-sm text-white/50 mt-2">Nhập URL và nhấn "Tải Xuống" để bắt đầu</p>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
169
+ <!-- Downloaded Files Section -->
170
+ <div class="glass-effect rounded-2xl p-8 mt-8">
171
+ <div class="flex items-center justify-between mb-6">
172
+ <h2 class="text-2xl font-semibold text-white flex items-center">
173
+ 📁 File Đã Tải
174
+ </h2>
175
+ <button
176
+ onclick="refreshDownloadsList()"
177
+ class="bg-white/10 hover:bg-white/20 text-white px-4 py-2 rounded-lg transition-colors text-sm"
178
+ >
179
+ 🔄 Làm mới
180
+ </button>
181
+ </div>
182
+
183
+ <div id="downloadsContainer">
184
+ <div class="bg-white/5 rounded-xl p-6 text-center">
185
+ <div class="text-4xl mb-4">�</div>
186
+ <p class="text-white/70">Đang tải danh sách file...</p>
187
+ </div>
188
+ </div>
189
+ </div>
190
+
191
+ <!-- Supported Sites -->
192
+ <div class="glass-effect rounded-2xl p-8 mt-8">
193
+ <h2 class="text-2xl font-semibold text-white mb-6 text-center">
194
+ 🌐 Trang Web Được Hỗ Trợ (yt-dlp)
195
+ </h2>
196
+ <div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
197
+ <div class="bg-white/10 rounded-lg p-4 text-center">
198
+ <div class="text-2xl mb-2">📺</div>
199
+ <span class="text-white/80 text-sm">YouTube</span>
200
+ </div>
201
+ <div class="bg-white/10 rounded-lg p-4 text-center">
202
+ <div class="text-2xl mb-2">📱</div>
203
+ <span class="text-white/80 text-sm">TikTok</span>
204
+ </div>
205
+ <div class="bg-white/10 rounded-lg p-4 text-center">
206
+ <div class="text-2xl mb-2">📘</div>
207
+ <span class="text-white/80 text-sm">Facebook</span>
208
+ </div>
209
+ <div class="bg-white/10 rounded-lg p-4 text-center">
210
+ <div class="text-2xl mb-2">📷</div>
211
+ <span class="text-white/80 text-sm">Instagram</span>
212
+ </div>
213
+ <div class="bg-white/10 rounded-lg p-4 text-center">
214
+ <div class="text-2xl mb-2">�</div>
215
+ <span class="text-white/80 text-sm">Twitter</span>
216
+ </div>
217
+ <div class="bg-white/10 rounded-lg p-4 text-center">
218
+ <div class="text-2xl mb-2">🎵</div>
219
+ <span class="text-white/80 text-sm">Vimeo</span>
220
+ </div>
221
+ </div>
222
+ <div class="text-center mt-4">
223
+ <p class="text-white/60 text-sm">và hàng nghìn trang web khác được hỗ trợ bởi yt-dlp</p>
224
+ </div>
225
+ </div>
226
+ </div>
227
+ </div>
228
+
229
+ <script>
230
+ const API_BASE = 'http://localhost:3000';
231
+ let currentVideoInfo = null;
232
+
233
+ // Handle radio button visual feedback
234
+ document.querySelectorAll('input[name="format"]').forEach(radio => {
235
+ radio.addEventListener('change', function() {
236
+ document.querySelectorAll('input[name="format"]').forEach(r => {
237
+ const circle = r.parentElement.querySelector('div div');
238
+ if (r.checked) {
239
+ circle.classList.add('bg-blue-400');
240
+ circle.classList.remove('bg-transparent');
241
+ } else {
242
+ circle.classList.remove('bg-blue-400');
243
+ circle.classList.add('bg-transparent');
244
+ }
245
+ });
246
+ });
247
+ });
248
+
249
+ // Show API status indicator
250
+ function showApiStatus(status, message) {
251
+ const statusDiv = document.getElementById('apiStatus');
252
+ const statusClass = status === 'ok' ? 'bg-green-500' : status === 'loading' ? 'bg-yellow-500' : 'bg-red-500';
253
+ statusDiv.innerHTML = `
254
+ <div class="${statusClass} text-white px-4 py-2 rounded-lg shadow-lg flex items-center">
255
+ <div class="mr-2">${status === 'ok' ? '✅' : status === 'loading' ? '⏳' : '❌'}</div>
256
+ <span class="text-sm">${message}</span>
257
+ </div>
258
+ `;
259
+ }
260
+
261
+ // Check API status on load
262
+ async function checkApiStatus() {
263
+ showApiStatus('loading', 'Đang kiểm tra API...');
264
+ try {
265
+ const response = await fetch(`${API_BASE}/status`);
266
+ const data = await response.json();
267
+ if (data.status === 'ok') {
268
+ showApiStatus('ok', `API sẵn sàng - yt-dlp ${data.yt_dlp_version}`);
269
+ } else {
270
+ showApiStatus('error', 'API có lỗi');
271
+ }
272
+ } catch (error) {
273
+ showApiStatus('error', 'Không thể kết nối API');
274
+ }
275
+ }
276
+
277
+ // Get video information
278
+ async function getVideoInfo() {
279
+ const url = document.getElementById('videoUrl').value.trim();
280
+ if (!url) {
281
+ alert('Vui lòng nhập URL video!');
282
+ return;
283
+ }
284
+
285
+ // Show loading state
286
+ const previewContainer = document.getElementById('previewContainer');
287
+ previewContainer.innerHTML = `
288
+ <div class="bg-gray-800 rounded-xl h-48 flex items-center justify-center">
289
+ <div class="text-center text-white">
290
+ <div class="text-4xl mb-4 spinner">⚙️</div>
291
+ <p>Đang lấy thông tin video...</p>
292
+ </div>
293
+ </div>
294
+ <div class="bg-white/5 rounded-xl p-4">
295
+ <div class="animate-pulse">
296
+ <div class="h-4 bg-white/20 rounded mb-2"></div>
297
+ <div class="h-3 bg-white/10 rounded"></div>
298
+ </div>
299
+ </div>
300
+ `;
301
+
302
+ try {
303
+ const response = await fetch(`${API_BASE}/video-info`, {
304
+ method: 'POST',
305
+ headers: { 'Content-Type': 'application/json' },
306
+ body: JSON.stringify({ url })
307
+ });
308
+
309
+ const data = await response.json();
310
+
311
+ if (response.ok) {
312
+ currentVideoInfo = data;
313
+ updatePreview(data);
314
+ } else {
315
+ showError(data.error || 'Không thể lấy thông tin video');
316
+ }
317
+ } catch (error) {
318
+ showError(`Lỗi kết nối: ${error.message}`);
319
+ }
320
+ }
321
+
322
+ // Update preview with video info
323
+ function updatePreview(videoInfo) {
324
+ const duration = videoInfo.duration ?
325
+ `${Math.floor(videoInfo.duration / 60)}:${(videoInfo.duration % 60).toString().padStart(2, '0')}` :
326
+ 'N/A';
327
+
328
+ const views = videoInfo.view_count ?
329
+ videoInfo.view_count.toLocaleString('vi-VN') :
330
+ 'N/A';
331
+
332
+ document.getElementById('previewContainer').innerHTML = `
333
+ <div class="bg-gray-800 rounded-xl h-48 flex items-center justify-center relative overflow-hidden">
334
+ ${videoInfo.thumbnail ?
335
+ `<img src="${videoInfo.thumbnail}" alt="Thumbnail" class="w-full h-full object-cover rounded-xl">` :
336
+ `<div class="text-center text-white">
337
+ <div class="text-6xl mb-2">🎬</div>
338
+ <p class="text-sm opacity-75">Video Preview</p>
339
+ </div>`
340
+ }
341
+ <div class="absolute inset-0 bg-black/20 rounded-xl"></div>
342
+ <div class="absolute bottom-4 right-4 bg-black/70 text-white px-2 py-1 rounded text-sm">
343
+ ${duration}
344
+ </div>
345
+ </div>
346
+ <div class="bg-white/5 rounded-xl p-4">
347
+ <h3 class="text-white font-medium mb-2">${videoInfo.title || 'Không có tiêu đề'}</h3>
348
+ <div class="flex justify-between text-sm text-white/70">
349
+ <span>Thời lượng: ${duration}</span>
350
+ <span>Lượt xem: ${views}</span>
351
+ </div>
352
+ <div class="mt-2 text-sm text-white/70">
353
+ <span>Kênh: ${videoInfo.uploader || 'N/A'}</span>
354
+ </div>
355
+ <div class="mt-2 text-xs text-white/50">
356
+ ${videoInfo.formats?.length || 0} định dạng có sẵn
357
+ </div>
358
+ </div>
359
+ `;
360
+ }
361
+
362
+ // Download video
363
+ async function downloadVideo() {
364
+ const url = document.getElementById('videoUrl').value.trim();
365
+ const quality = document.getElementById('qualitySelect').value;
366
+ const format = document.querySelector('input[name="format"]:checked').value;
367
+
368
+ if (!url) {
369
+ alert('Vui lòng nhập URL video!');
370
+ return;
371
+ }
372
+
373
+ // Show loading state
374
+ const outputContainer = document.getElementById('outputContainer');
375
+ outputContainer.innerHTML = `
376
+ <div class="bg-blue-500/10 border border-blue-500/30 rounded-xl p-6 text-center">
377
+ <div class="text-4xl mb-4 spinner">⚙️</div>
378
+ <p class="text-white mb-2">Đang tải video...</p>
379
+ <p class="text-white/70 text-sm">Chất lượng: ${quality} | Định dạng: ${format === 'audio' ? 'MP3' : 'Video'}</p>
380
+ <div class="w-full bg-white/10 rounded-full h-2 mt-4">
381
+ <div class="bg-blue-500 h-2 rounded-full animate-pulse" style="width: 60%"></div>
382
+ </div>
383
+ <p class="text-white/60 text-xs mt-2">Thời gian tải phụ thuộc vào kích thước và chất lượng video</p>
384
+ </div>
385
+ `;
386
+
387
+ try {
388
+ const response = await fetch(`${API_BASE}/download`, {
389
+ method: 'POST',
390
+ headers: { 'Content-Type': 'application/json' },
391
+ body: JSON.stringify({ url, format, quality })
392
+ });
393
+
394
+ const data = await response.json();
395
+
396
+ if (response.ok) {
397
+ showDownloadSuccess(data);
398
+ refreshDownloadsList();
399
+ } else {
400
+ showError(data.error || 'Không thể tải video');
401
+ }
402
+ } catch (error) {
403
+ showError(`Lỗi kết nối: ${error.message}`);
404
+ }
405
+ }
406
+
407
+ // Show download success
408
+ function showDownloadSuccess(data) {
409
+ const outputContainer = document.getElementById('outputContainer');
410
+ outputContainer.innerHTML = `
411
+ <div class="bg-green-500/10 border border-green-500/30 rounded-xl p-6">
412
+ <div class="flex items-center justify-between mb-4">
413
+ <div class="flex items-center">
414
+ <div class="text-2xl mr-3">✅</div>
415
+ <div>
416
+ <h3 class="text-white font-medium">Tải xuống thành công!</h3>
417
+ <p class="text-white/70 text-sm">${data.message}</p>
418
+ </div>
419
+ </div>
420
+ </div>
421
+
422
+ <div class="bg-white/5 rounded-lg p-4 mb-4">
423
+ <div class="flex justify-between items-center">
424
+ <div>
425
+ <p class="text-white font-medium">${data.filename}</p>
426
+ <p class="text-white/70 text-sm">File đã được lưu trên server</p>
427
+ </div>
428
+ <a href="${API_BASE}${data.download_url}"
429
+ target="_blank"
430
+ class="bg-blue-500 hover:bg-blue-600 text-white px-6 py-2 rounded-lg transition-colors">
431
+ 📥 Tải về máy
432
+ </a>
433
+ </div>
434
+ </div>
435
+
436
+ <div class="text-center">
437
+ <p class="text-white/60 text-sm">
438
+ 💡 File sẽ được lưu trên server và có thể tải về từ danh sách "File Đã Tải"
439
+ </p>
440
+ </div>
441
+ </div>
442
+ `;
443
+ }
444
+
445
+ // Show error
446
+ function showError(message) {
447
+ const outputContainer = document.getElementById('outputContainer');
448
+ outputContainer.innerHTML = `
449
+ <div class="bg-red-500/10 border border-red-500/30 rounded-xl p-6 text-center">
450
+ <div class="text-4xl mb-4">❌</div>
451
+ <p class="text-white mb-2">Có lỗi xảy ra</p>
452
+ <p class="text-red-300 text-sm">${message}</p>
453
+ <button onclick="checkApiStatus()" class="mt-4 bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg transition-colors text-sm">
454
+ 🔄 Kiểm tra lại API
455
+ </button>
456
+ </div>
457
+ `;
458
+ }
459
+
460
+ // Refresh downloads list
461
+ async function refreshDownloadsList() {
462
+ const container = document.getElementById('downloadsContainer');
463
+ container.innerHTML = `
464
+ <div class="bg-white/5 rounded-xl p-6 text-center">
465
+ <div class="text-4xl mb-4 spinner">⚙️</div>
466
+ <p class="text-white/70">Đang tải danh sách file...</p>
467
+ </div>
468
+ `;
469
+
470
+ try {
471
+ const response = await fetch(`${API_BASE}/downloads`);
472
+ const files = await response.json();
473
+
474
+ if (files.length === 0) {
475
+ container.innerHTML = `
476
+ <div class="bg-white/5 rounded-xl p-6 text-center">
477
+ <div class="text-4xl mb-4">📂</div>
478
+ <p class="text-white/70">Chưa có file nào được tải về</p>
479
+ <p class="text-white/50 text-sm mt-2">Tải video để xem danh sách ở đây</p>
480
+ </div>
481
+ `;
482
+ } else {
483
+ let filesHTML = '<div class="space-y-3">';
484
+ files.forEach((file, index) => {
485
+ const size = (file.size / (1024 * 1024)).toFixed(2);
486
+ const date = new Date(file.created).toLocaleString('vi-VN');
487
+ filesHTML += `
488
+ <div class="bg-white/5 rounded-lg p-4">
489
+ <div class="flex justify-between items-center">
490
+ <div class="flex-1">
491
+ <h4 class="text-white font-medium text-sm mb-1">${file.filename}</h4>
492
+ <div class="text-white/70 text-xs">
493
+ <span>${size} MB</span> •
494
+ <span>${date}</span>
495
+ </div>
496
+ </div>
497
+ <div class="flex gap-2 ml-4">
498
+ <a href="${API_BASE}${file.download_url}"
499
+ target="_blank"
500
+ class="bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded text-xs transition-colors">
501
+ 📥 Tải
502
+ </a>
503
+ <button onclick="deleteFile('${file.filename}')"
504
+ class="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded text-xs transition-colors">
505
+ 🗑️ Xóa
506
+ </button>
507
+ </div>
508
+ </div>
509
+ </div>
510
+ `;
511
+ });
512
+ filesHTML += '</div>';
513
+ container.innerHTML = filesHTML;
514
+ }
515
+ } catch (error) {
516
+ container.innerHTML = `
517
+ <div class="bg-red-500/10 border border-red-500/30 rounded-xl p-6 text-center">
518
+ <div class="text-4xl mb-4">❌</div>
519
+ <p class="text-white/70">Không thể tải danh sách file</p>
520
+ <p class="text-red-300 text-sm">${error.message}</p>
521
+ </div>
522
+ `;
523
+ }
524
+ }
525
+
526
+ // Delete file
527
+ async function deleteFile(filename) {
528
+ if (!confirm(`Bạn có chắc muốn xóa file "${filename}"?`)) {
529
+ return;
530
+ }
531
+
532
+ try {
533
+ const response = await fetch(`${API_BASE}/delete/${encodeURIComponent(filename)}`, {
534
+ method: 'DELETE'
535
+ });
536
+
537
+ if (response.ok) {
538
+ refreshDownloadsList();
539
+ } else {
540
+ const data = await response.json();
541
+ alert(`Lỗi: ${data.error}`);
542
+ }
543
+ } catch (error) {
544
+ alert(`Lỗi kết nối: ${error.message}`);
545
+ }
546
+ }
547
+
548
+ // Initialize app
549
+ window.onload = function() {
550
+ checkApiStatus();
551
+ refreshDownloadsList();
552
+ document.getElementById('videoUrl').focus();
553
+ };
554
+ </script>
555
+ </body>
556
+ </html>
demo.html ADDED
@@ -0,0 +1,500 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Video Downloader Online - yt-dlp API</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
10
+ body { font-family: 'Inter', sans-serif; }
11
+ .gradient-bg {
12
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
13
+ }
14
+ .glass-effect {
15
+ background: rgba(255, 255, 255, 0.1);
16
+ backdrop-filter: blur(10px);
17
+ border: 1px solid rgba(255, 255, 255, 0.2);
18
+ }
19
+ .preview-placeholder {
20
+ background: linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
21
+ linear-gradient(-45deg, #f0f0f0 25%, transparent 25%),
22
+ linear-gradient(45deg, transparent 75%, #f0f0f0 75%),
23
+ linear-gradient(-45deg, transparent 75%, #f0f0f0 75%);
24
+ background-size: 20px 20px;
25
+ background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
26
+ }
27
+ .spinner {
28
+ animation: spin 1s linear infinite;
29
+ }
30
+ @keyframes spin {
31
+ from { transform: rotate(0deg); }
32
+ to { transform: rotate(360deg); }
33
+ }
34
+ video {
35
+ max-width: 100%;
36
+ height: auto;
37
+ }
38
+ </style>
39
+ </head>
40
+ <body class="gradient-bg min-h-screen">
41
+ <!-- API Status Indicator -->
42
+ <div id="apiStatus" class="fixed top-4 right-4 z-50"></div>
43
+
44
+ <div class="container mx-auto px-4 py-8">
45
+ <!-- Header -->
46
+ <div class="text-center mb-12">
47
+ <h1 class="text-5xl font-bold text-white mb-4">
48
+ 🎬 Video Downloader
49
+ </h1>
50
+ <p class="text-xl text-white/80 max-w-2xl mx-auto">
51
+ Tải video từ YouTube và các trang web khác sử dụng yt-dlp API
52
+ </p>
53
+ </div>
54
+
55
+ <!-- Main Content -->
56
+ <div class="max-w-6xl mx-auto">
57
+ <div class="grid lg:grid-cols-2 gap-8">
58
+ <!-- Input Section -->
59
+ <div class="glass-effect rounded-2xl p-8">
60
+ <h2 class="text-2xl font-semibold text-white mb-6 flex items-center">
61
+ 📥 Nhập Liên Kết
62
+ </h2>
63
+
64
+ <div class="space-y-6">
65
+ <div>
66
+ <label class="block text-white/90 text-sm font-medium mb-3">
67
+ URL Video
68
+ </label>
69
+ <input
70
+ type="url"
71
+ id="videoUrl"
72
+ placeholder="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
73
+ value="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
74
+ class="w-full px-4 py-3 rounded-xl bg-white/10 border border-white/20 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all"
75
+ >
76
+ </div>
77
+
78
+ <div>
79
+ <label class="block text-white/90 text-sm font-medium mb-3">
80
+ Chất Lượng
81
+ </label>
82
+ <select id="qualitySelect" class="w-full px-4 py-3 rounded-xl bg-white/10 border border-white/20 text-white focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all">
83
+ <option value="best" class="bg-gray-800">Tốt nhất</option>
84
+ <option value="worst" class="bg-gray-800">Thấp nhất (nhanh hơn)</option>
85
+ <option value="720p" class="bg-gray-800">720p (HD)</option>
86
+ <option value="480p" class="bg-gray-800">480p (SD)</option>
87
+ <option value="360p" class="bg-gray-800">360p</option>
88
+ </select>
89
+ </div>
90
+
91
+ <div>
92
+ <label class="block text-white/90 text-sm font-medium mb-3">
93
+ Định Dạng
94
+ </label>
95
+ <div class="flex gap-3">
96
+ <label class="flex items-center cursor-pointer">
97
+ <input type="radio" name="format" value="video" checked class="sr-only">
98
+ <div class="w-5 h-5 rounded-full border-2 border-white/40 flex items-center justify-center mr-2">
99
+ <div class="w-2 h-2 rounded-full bg-blue-400"></div>
100
+ </div>
101
+ <span class="text-white/90">Video</span>
102
+ </label>
103
+ <label class="flex items-center cursor-pointer">
104
+ <input type="radio" name="format" value="audio" class="sr-only">
105
+ <div class="w-5 h-5 rounded-full border-2 border-white/40 flex items-center justify-center mr-2">
106
+ <div class="w-2 h-2 rounded-full bg-transparent"></div>
107
+ </div>
108
+ <span class="text-white/90">Audio MP3</span>
109
+ </label>
110
+ </div>
111
+ </div>
112
+
113
+ <div class="flex gap-3">
114
+ <button
115
+ onclick="getVideoInfo()"
116
+ class="flex-1 bg-gradient-to-r from-green-500 to-teal-600 hover:from-green-600 hover:to-teal-700 text-white font-semibold py-4 px-6 rounded-xl transition-all duration-300 transform hover:scale-105 shadow-lg"
117
+ >
118
+ 🔍 Xem Thông Tin
119
+ </button>
120
+ <button
121
+ onclick="downloadVideo()"
122
+ class="flex-1 bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white font-semibold py-4 px-6 rounded-xl transition-all duration-300 transform hover:scale-105 shadow-lg"
123
+ >
124
+ 🚀 Tải Xuống
125
+ </button>
126
+ </div>
127
+ </div>
128
+ </div>
129
+
130
+ <!-- Preview Section -->
131
+ <div class="glass-effect rounded-2xl p-8">
132
+ <h2 class="text-2xl font-semibold text-white mb-6 flex items-center">
133
+ 👁️ Thông Tin Video
134
+ </h2>
135
+
136
+ <div id="previewContainer" class="space-y-4">
137
+ <div class="preview-placeholder rounded-xl h-48 flex items-center justify-center">
138
+ <div class="text-center text-gray-500">
139
+ <div class="text-4xl mb-2">🎥</div>
140
+ <p>Nhấn "Xem Thông Tin" để hiển thị chi tiết video</p>
141
+ </div>
142
+ </div>
143
+
144
+ <div class="bg-white/5 rounded-xl p-4">
145
+ <h3 id="videoTitle" class="text-white font-medium mb-2">Tiêu đề video sẽ hiển thị ở đây</h3>
146
+ <div class="flex justify-between text-sm text-white/70">
147
+ <span id="videoDuration">Thời lượng: --:--</span>
148
+ <span id="videoViews">Lượt xem: --</span>
149
+ </div>
150
+ <div class="mt-2 text-sm text-white/70">
151
+ <span id="videoUploader">Kênh: --</span>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ </div>
157
+
158
+ <!-- Output Section -->
159
+ <div class="glass-effect rounded-2xl p-8 mt-8">
160
+ <h2 class="text-2xl font-semibold text-white mb-6 flex items-center">
161
+ 📤 Kết Quả Tải Xuống
162
+ </h2>
163
+
164
+ <div id="outputContainer" class="space-y-4">
165
+ <div class="bg-white/5 rounded-xl p-6 text-center">
166
+ <div class="text-4xl mb-4">⏳</div>
167
+ <p class="text-white/70">Chưa có video nào được xử lý</p>
168
+ <p class="text-sm text-white/50 mt-2">Nhập URL và nhấn "Tải Xuống" để bắt đầu</p>
169
+ </div>
170
+ </div>
171
+ </div>
172
+
173
+ <!-- Supported Sites -->
174
+ <div class="glass-effect rounded-2xl p-8 mt-8">
175
+ <h2 class="text-2xl font-semibold text-white mb-6 text-center">
176
+ 🌐 Trang Web Được Hỗ Trợ (yt-dlp)
177
+ </h2>
178
+ <div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
179
+ <div class="bg-white/10 rounded-lg p-4 text-center">
180
+ <div class="text-2xl mb-2">📺</div>
181
+ <span class="text-white/80 text-sm">YouTube</span>
182
+ </div>
183
+ <div class="bg-white/10 rounded-lg p-4 text-center">
184
+ <div class="text-2xl mb-2">📱</div>
185
+ <span class="text-white/80 text-sm">TikTok</span>
186
+ </div>
187
+ <div class="bg-white/10 rounded-lg p-4 text-center">
188
+ <div class="text-2xl mb-2">📘</div>
189
+ <span class="text-white/80 text-sm">Facebook</span>
190
+ </div>
191
+ <div class="bg-white/10 rounded-lg p-4 text-center">
192
+ <div class="text-2xl mb-2">📷</div>
193
+ <span class="text-white/80 text-sm">Instagram</span>
194
+ </div>
195
+ <div class="bg-white/10 rounded-lg p-4 text-center">
196
+ <div class="text-2xl mb-2">🐦</div>
197
+ <span class="text-white/80 text-sm">Twitter</span>
198
+ </div>
199
+ <div class="bg-white/10 rounded-lg p-4 text-center">
200
+ <div class="text-2xl mb-2">🎵</div>
201
+ <span class="text-white/80 text-sm">Vimeo</span>
202
+ </div>
203
+ </div>
204
+ <div class="text-center mt-4">
205
+ <p class="text-white/60 text-sm">và hàng nghìn trang web khác được hỗ trợ bởi yt-dlp</p>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ </div>
210
+
211
+ <script>
212
+ // Auto-detect API base URL
213
+ const API_BASE = window.location.origin;
214
+ let currentVideoInfo = null;
215
+
216
+ // Handle radio button visual feedback
217
+ document.querySelectorAll('input[name="format"]').forEach(radio => {
218
+ radio.addEventListener('change', function() {
219
+ document.querySelectorAll('input[name="format"]').forEach(r => {
220
+ const circle = r.parentElement.querySelector('div div');
221
+ if (r.checked) {
222
+ circle.classList.add('bg-blue-400');
223
+ circle.classList.remove('bg-transparent');
224
+ } else {
225
+ circle.classList.remove('bg-blue-400');
226
+ circle.classList.add('bg-transparent');
227
+ }
228
+ });
229
+ });
230
+ });
231
+
232
+ // Show API status indicator
233
+ function showApiStatus(status, message) {
234
+ const statusDiv = document.getElementById('apiStatus');
235
+ const statusClass = status === 'ok' ? 'bg-green-500' : status === 'loading' ? 'bg-yellow-500' : 'bg-red-500';
236
+ statusDiv.innerHTML = `
237
+ <div class="${statusClass} text-white px-4 py-2 rounded-lg shadow-lg flex items-center">
238
+ <div class="mr-2">${status === 'ok' ? '✅' : status === 'loading' ? '⏳' : '❌'}</div>
239
+ <span class="text-sm">${message}</span>
240
+ </div>
241
+ `;
242
+ }
243
+
244
+ // Check API status on load
245
+ async function checkApiStatus() {
246
+ showApiStatus('loading', 'Đang kiểm tra API...');
247
+ try {
248
+ const response = await fetch(`${API_BASE}/status`);
249
+ const data = await response.json();
250
+ if (data.status === 'ok') {
251
+ showApiStatus('ok', `API sẵn sàng - yt-dlp ${data.yt_dlp_version}`);
252
+ } else {
253
+ showApiStatus('error', 'API có lỗi');
254
+ }
255
+ } catch (error) {
256
+ showApiStatus('error', 'Không thể kết nối API');
257
+ }
258
+ }
259
+
260
+ // Get video information
261
+ async function getVideoInfo() {
262
+ const url = document.getElementById('videoUrl').value.trim();
263
+ if (!url) {
264
+ alert('Vui lòng nhập URL video!');
265
+ return;
266
+ }
267
+
268
+ // Show loading state
269
+ const previewContainer = document.getElementById('previewContainer');
270
+ previewContainer.innerHTML = `
271
+ <div class="bg-gray-800 rounded-xl h-48 flex items-center justify-center">
272
+ <div class="text-center text-white">
273
+ <div class="text-4xl mb-4 spinner">⚙️</div>
274
+ <p>Đang lấy thông tin video...</p>
275
+ </div>
276
+ </div>
277
+ <div class="bg-white/5 rounded-xl p-4">
278
+ <div class="animate-pulse">
279
+ <div class="h-4 bg-white/20 rounded mb-2"></div>
280
+ <div class="h-3 bg-white/10 rounded"></div>
281
+ </div>
282
+ </div>
283
+ `;
284
+
285
+ try {
286
+ const response = await fetch(`${API_BASE}/video-info`, {
287
+ method: 'POST',
288
+ headers: { 'Content-Type': 'application/json' },
289
+ body: JSON.stringify({ url })
290
+ });
291
+
292
+ const data = await response.json();
293
+
294
+ if (response.ok) {
295
+ currentVideoInfo = data;
296
+ updatePreview(data);
297
+ } else {
298
+ showError(data.error || 'Không thể lấy thông tin video');
299
+ }
300
+ } catch (error) {
301
+ showError(`Lỗi kết nối: ${error.message}`);
302
+ }
303
+ }
304
+
305
+ // Update preview with video info
306
+ function updatePreview(videoInfo) {
307
+ const duration = videoInfo.duration ?
308
+ `${Math.floor(videoInfo.duration / 60)}:${(videoInfo.duration % 60).toString().padStart(2, '0')}` :
309
+ 'N/A';
310
+
311
+ const views = videoInfo.view_count ?
312
+ videoInfo.view_count.toLocaleString('vi-VN') :
313
+ 'N/A';
314
+
315
+ document.getElementById('previewContainer').innerHTML = `
316
+ <div class="bg-gray-800 rounded-xl h-48 flex items-center justify-center relative overflow-hidden">
317
+ ${videoInfo.thumbnail ?
318
+ `<img src="${videoInfo.thumbnail}" alt="Thumbnail" class="w-full h-full object-cover rounded-xl">` :
319
+ `<div class="text-center text-white">
320
+ <div class="text-6xl mb-2">🎬</div>
321
+ <p class="text-sm opacity-75">Video Preview</p>
322
+ </div>`
323
+ }
324
+ <div class="absolute inset-0 bg-black/20 rounded-xl"></div>
325
+ <div class="absolute bottom-4 right-4 bg-black/70 text-white px-2 py-1 rounded text-sm">
326
+ ${duration}
327
+ </div>
328
+ </div>
329
+ <div class="bg-white/5 rounded-xl p-4">
330
+ <h3 class="text-white font-medium mb-2">${videoInfo.title || 'Không có tiêu đề'}</h3>
331
+ <div class="flex justify-between text-sm text-white/70">
332
+ <span>Thời lượng: ${duration}</span>
333
+ <span>Lượt xem: ${views}</span>
334
+ </div>
335
+ <div class="mt-2 text-sm text-white/70">
336
+ <span>Kênh: ${videoInfo.uploader || 'N/A'}</span>
337
+ </div>
338
+ <div class="mt-2 text-xs text-white/50">
339
+ ${videoInfo.formats?.length || 0} định dạng có sẵn
340
+ </div>
341
+ </div>
342
+ `;
343
+ }
344
+
345
+ // Download video
346
+ async function downloadVideo() {
347
+ const url = document.getElementById('videoUrl').value.trim();
348
+ const quality = document.getElementById('qualitySelect').value;
349
+ const format = document.querySelector('input[name="format"]:checked').value;
350
+
351
+ if (!url) {
352
+ alert('Vui lòng nhập URL video!');
353
+ return;
354
+ }
355
+
356
+ // Show loading state
357
+ const outputContainer = document.getElementById('outputContainer');
358
+ outputContainer.innerHTML = `
359
+ <div class="bg-blue-500/10 border border-blue-500/30 rounded-xl p-6 text-center">
360
+ <div class="text-4xl mb-4 spinner">⚙️</div>
361
+ <p class="text-white mb-2">Đang tải video...</p>
362
+ <p class="text-white/70 text-sm">Chất lượng: ${quality} | Định dạng: ${format === 'audio' ? 'MP3' : 'Video'}</p>
363
+ <div class="w-full bg-white/10 rounded-full h-2 mt-4">
364
+ <div class="bg-blue-500 h-2 rounded-full animate-pulse" style="width: 60%"></div>
365
+ </div>
366
+ <p class="text-white/60 text-xs mt-2">Thời gian tải phụ thuộc vào kích thước và chất lượng video</p>
367
+ </div>
368
+ `;
369
+
370
+ try {
371
+ const response = await fetch(`${API_BASE}/download`, {
372
+ method: 'POST',
373
+ headers: { 'Content-Type': 'application/json' },
374
+ body: JSON.stringify({ url, format, quality })
375
+ });
376
+
377
+ const data = await response.json();
378
+
379
+ if (response.ok) {
380
+ showDownloadSuccess(data);
381
+ } else {
382
+ showError(data.error || 'Không thể tải video');
383
+ }
384
+ } catch (error) {
385
+ showError(`Lỗi kết nối: ${error.message}`);
386
+ }
387
+ }
388
+
389
+ // Show download success with video player
390
+ function showDownloadSuccess(data) {
391
+ const outputContainer = document.getElementById('outputContainer');
392
+ const isVideo = data.format === 'video';
393
+ const fileSize = (data.size / (1024 * 1024)).toFixed(2);
394
+
395
+ outputContainer.innerHTML = `
396
+ <div class="bg-green-500/10 border border-green-500/30 rounded-xl p-6">
397
+ <div class="flex items-center justify-between mb-4">
398
+ <div class="flex items-center">
399
+ <div class="text-2xl mr-3">✅</div>
400
+ <div>
401
+ <h3 class="text-white font-medium">Tải xuống thành công!</h3>
402
+ <p class="text-white/70 text-sm">${data.originalTitle}</p>
403
+ </div>
404
+ </div>
405
+ </div>
406
+
407
+ ${isVideo ? `
408
+ <div class="bg-black rounded-xl p-4 mb-4">
409
+ <video controls class="w-full rounded-lg" poster="${data.thumbnail || ''}">
410
+ <source src="${data.direct_link}" type="video/mp4">
411
+ Trình duyệt của bạn không hỗ trợ video HTML5.
412
+ </video>
413
+ </div>
414
+ ` : `
415
+ <div class="bg-black/20 rounded-xl p-4 mb-4 flex items-center justify-center">
416
+ <div class="text-center text-white">
417
+ <div class="text-6xl mb-2">🎵</div>
418
+ <p class="text-lg font-medium">Audio MP3</p>
419
+ <audio controls class="mt-4">
420
+ <source src="${data.direct_link}" type="audio/mpeg">
421
+ Trình duyệt của bạn không hỗ trợ audio HTML5.
422
+ </audio>
423
+ </div>
424
+ </div>
425
+ `}
426
+
427
+ <div class="bg-white/5 rounded-lg p-4 mb-4">
428
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm text-white/70">
429
+ <div>
430
+ <span class="block font-medium text-white">ID File:</span>
431
+ ${data.fileId}
432
+ </div>
433
+ <div>
434
+ <span class="block font-medium text-white">Kích thước:</span>
435
+ ${fileSize} MB
436
+ </div>
437
+ <div>
438
+ <span class="block font-medium text-white">Định dạng:</span>
439
+ ${data.format === 'audio' ? 'MP3' : 'MP4'}
440
+ </div>
441
+ <div>
442
+ <span class="block font-medium text-white">Thời gian xóa:</span>
443
+ ${data.expires_in}
444
+ </div>
445
+ </div>
446
+ </div>
447
+
448
+ <div class="flex gap-3">
449
+ <a href="${data.direct_link}"
450
+ download="${data.filename}"
451
+ class="flex-1 bg-blue-500 hover:bg-blue-600 text-white px-6 py-3 rounded-lg transition-colors text-center font-medium">
452
+ 📥 Tải về máy
453
+ </a>
454
+ <button onclick="copyLink('${data.direct_link}')"
455
+ class="flex-1 bg-purple-500 hover:bg-purple-600 text-white px-6 py-3 rounded-lg transition-colors font-medium">
456
+ 🔗 Copy Link
457
+ </button>
458
+ </div>
459
+
460
+ <div class="text-center mt-4">
461
+ <p class="text-white/60 text-sm">
462
+ ⚠️ File sẽ tự động xóa sau ${data.expires_in} kể từ khi tải xuống
463
+ </p>
464
+ </div>
465
+ </div>
466
+ `;
467
+ }
468
+
469
+ // Copy link to clipboard
470
+ function copyLink(link) {
471
+ navigator.clipboard.writeText(link).then(() => {
472
+ alert('Link đã được copy vào clipboard!');
473
+ }).catch(() => {
474
+ alert('Không thể copy link. Hãy copy thủ công: ' + link);
475
+ });
476
+ }
477
+
478
+ // Show error
479
+ function showError(message) {
480
+ const outputContainer = document.getElementById('outputContainer');
481
+ outputContainer.innerHTML = `
482
+ <div class="bg-red-500/10 border border-red-500/30 rounded-xl p-6 text-center">
483
+ <div class="text-4xl mb-4">❌</div>
484
+ <p class="text-white mb-2">Có lỗi xảy ra</p>
485
+ <p class="text-red-300 text-sm">${message}</p>
486
+ <button onclick="checkApiStatus()" class="mt-4 bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg transition-colors text-sm">
487
+ 🔄 Kiểm tra lại API
488
+ </button>
489
+ </div>
490
+ `;
491
+ }
492
+
493
+ // Initialize app
494
+ window.onload = function() {
495
+ checkApiStatus();
496
+ document.getElementById('videoUrl').focus();
497
+ };
498
+ </script>
499
+ </body>
500
+ </html>
package-lock.json ADDED
@@ -0,0 +1,1064 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "ytdlp",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "ytdlp",
9
+ "version": "1.0.0",
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "cors": "^2.8.5",
13
+ "express": "^5.1.0",
14
+ "multer": "^2.0.2",
15
+ "node-fetch": "^2.7.0"
16
+ }
17
+ },
18
+ "node_modules/accepts": {
19
+ "version": "2.0.0",
20
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
21
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "mime-types": "^3.0.0",
25
+ "negotiator": "^1.0.0"
26
+ },
27
+ "engines": {
28
+ "node": ">= 0.6"
29
+ }
30
+ },
31
+ "node_modules/append-field": {
32
+ "version": "1.0.0",
33
+ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
34
+ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
35
+ "license": "MIT"
36
+ },
37
+ "node_modules/body-parser": {
38
+ "version": "2.2.0",
39
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
40
+ "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
41
+ "license": "MIT",
42
+ "dependencies": {
43
+ "bytes": "^3.1.2",
44
+ "content-type": "^1.0.5",
45
+ "debug": "^4.4.0",
46
+ "http-errors": "^2.0.0",
47
+ "iconv-lite": "^0.6.3",
48
+ "on-finished": "^2.4.1",
49
+ "qs": "^6.14.0",
50
+ "raw-body": "^3.0.0",
51
+ "type-is": "^2.0.0"
52
+ },
53
+ "engines": {
54
+ "node": ">=18"
55
+ }
56
+ },
57
+ "node_modules/buffer-from": {
58
+ "version": "1.1.2",
59
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
60
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
61
+ "license": "MIT"
62
+ },
63
+ "node_modules/busboy": {
64
+ "version": "1.6.0",
65
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
66
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
67
+ "dependencies": {
68
+ "streamsearch": "^1.1.0"
69
+ },
70
+ "engines": {
71
+ "node": ">=10.16.0"
72
+ }
73
+ },
74
+ "node_modules/bytes": {
75
+ "version": "3.1.2",
76
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
77
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
78
+ "license": "MIT",
79
+ "engines": {
80
+ "node": ">= 0.8"
81
+ }
82
+ },
83
+ "node_modules/call-bind-apply-helpers": {
84
+ "version": "1.0.2",
85
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
86
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
87
+ "license": "MIT",
88
+ "dependencies": {
89
+ "es-errors": "^1.3.0",
90
+ "function-bind": "^1.1.2"
91
+ },
92
+ "engines": {
93
+ "node": ">= 0.4"
94
+ }
95
+ },
96
+ "node_modules/call-bound": {
97
+ "version": "1.0.4",
98
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
99
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
100
+ "license": "MIT",
101
+ "dependencies": {
102
+ "call-bind-apply-helpers": "^1.0.2",
103
+ "get-intrinsic": "^1.3.0"
104
+ },
105
+ "engines": {
106
+ "node": ">= 0.4"
107
+ },
108
+ "funding": {
109
+ "url": "https://github.com/sponsors/ljharb"
110
+ }
111
+ },
112
+ "node_modules/concat-stream": {
113
+ "version": "2.0.0",
114
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
115
+ "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
116
+ "engines": [
117
+ "node >= 6.0"
118
+ ],
119
+ "license": "MIT",
120
+ "dependencies": {
121
+ "buffer-from": "^1.0.0",
122
+ "inherits": "^2.0.3",
123
+ "readable-stream": "^3.0.2",
124
+ "typedarray": "^0.0.6"
125
+ }
126
+ },
127
+ "node_modules/content-disposition": {
128
+ "version": "1.0.0",
129
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
130
+ "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
131
+ "license": "MIT",
132
+ "dependencies": {
133
+ "safe-buffer": "5.2.1"
134
+ },
135
+ "engines": {
136
+ "node": ">= 0.6"
137
+ }
138
+ },
139
+ "node_modules/content-type": {
140
+ "version": "1.0.5",
141
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
142
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
143
+ "license": "MIT",
144
+ "engines": {
145
+ "node": ">= 0.6"
146
+ }
147
+ },
148
+ "node_modules/cookie": {
149
+ "version": "0.7.2",
150
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
151
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
152
+ "license": "MIT",
153
+ "engines": {
154
+ "node": ">= 0.6"
155
+ }
156
+ },
157
+ "node_modules/cookie-signature": {
158
+ "version": "1.2.2",
159
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
160
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
161
+ "license": "MIT",
162
+ "engines": {
163
+ "node": ">=6.6.0"
164
+ }
165
+ },
166
+ "node_modules/cors": {
167
+ "version": "2.8.5",
168
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
169
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
170
+ "license": "MIT",
171
+ "dependencies": {
172
+ "object-assign": "^4",
173
+ "vary": "^1"
174
+ },
175
+ "engines": {
176
+ "node": ">= 0.10"
177
+ }
178
+ },
179
+ "node_modules/debug": {
180
+ "version": "4.4.1",
181
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
182
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
183
+ "license": "MIT",
184
+ "dependencies": {
185
+ "ms": "^2.1.3"
186
+ },
187
+ "engines": {
188
+ "node": ">=6.0"
189
+ },
190
+ "peerDependenciesMeta": {
191
+ "supports-color": {
192
+ "optional": true
193
+ }
194
+ }
195
+ },
196
+ "node_modules/depd": {
197
+ "version": "2.0.0",
198
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
199
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
200
+ "license": "MIT",
201
+ "engines": {
202
+ "node": ">= 0.8"
203
+ }
204
+ },
205
+ "node_modules/dunder-proto": {
206
+ "version": "1.0.1",
207
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
208
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
209
+ "license": "MIT",
210
+ "dependencies": {
211
+ "call-bind-apply-helpers": "^1.0.1",
212
+ "es-errors": "^1.3.0",
213
+ "gopd": "^1.2.0"
214
+ },
215
+ "engines": {
216
+ "node": ">= 0.4"
217
+ }
218
+ },
219
+ "node_modules/ee-first": {
220
+ "version": "1.1.1",
221
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
222
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
223
+ "license": "MIT"
224
+ },
225
+ "node_modules/encodeurl": {
226
+ "version": "2.0.0",
227
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
228
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
229
+ "license": "MIT",
230
+ "engines": {
231
+ "node": ">= 0.8"
232
+ }
233
+ },
234
+ "node_modules/es-define-property": {
235
+ "version": "1.0.1",
236
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
237
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
238
+ "license": "MIT",
239
+ "engines": {
240
+ "node": ">= 0.4"
241
+ }
242
+ },
243
+ "node_modules/es-errors": {
244
+ "version": "1.3.0",
245
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
246
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
247
+ "license": "MIT",
248
+ "engines": {
249
+ "node": ">= 0.4"
250
+ }
251
+ },
252
+ "node_modules/es-object-atoms": {
253
+ "version": "1.1.1",
254
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
255
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
256
+ "license": "MIT",
257
+ "dependencies": {
258
+ "es-errors": "^1.3.0"
259
+ },
260
+ "engines": {
261
+ "node": ">= 0.4"
262
+ }
263
+ },
264
+ "node_modules/escape-html": {
265
+ "version": "1.0.3",
266
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
267
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
268
+ "license": "MIT"
269
+ },
270
+ "node_modules/etag": {
271
+ "version": "1.8.1",
272
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
273
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
274
+ "license": "MIT",
275
+ "engines": {
276
+ "node": ">= 0.6"
277
+ }
278
+ },
279
+ "node_modules/express": {
280
+ "version": "5.1.0",
281
+ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
282
+ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
283
+ "license": "MIT",
284
+ "dependencies": {
285
+ "accepts": "^2.0.0",
286
+ "body-parser": "^2.2.0",
287
+ "content-disposition": "^1.0.0",
288
+ "content-type": "^1.0.5",
289
+ "cookie": "^0.7.1",
290
+ "cookie-signature": "^1.2.1",
291
+ "debug": "^4.4.0",
292
+ "encodeurl": "^2.0.0",
293
+ "escape-html": "^1.0.3",
294
+ "etag": "^1.8.1",
295
+ "finalhandler": "^2.1.0",
296
+ "fresh": "^2.0.0",
297
+ "http-errors": "^2.0.0",
298
+ "merge-descriptors": "^2.0.0",
299
+ "mime-types": "^3.0.0",
300
+ "on-finished": "^2.4.1",
301
+ "once": "^1.4.0",
302
+ "parseurl": "^1.3.3",
303
+ "proxy-addr": "^2.0.7",
304
+ "qs": "^6.14.0",
305
+ "range-parser": "^1.2.1",
306
+ "router": "^2.2.0",
307
+ "send": "^1.1.0",
308
+ "serve-static": "^2.2.0",
309
+ "statuses": "^2.0.1",
310
+ "type-is": "^2.0.1",
311
+ "vary": "^1.1.2"
312
+ },
313
+ "engines": {
314
+ "node": ">= 18"
315
+ },
316
+ "funding": {
317
+ "type": "opencollective",
318
+ "url": "https://opencollective.com/express"
319
+ }
320
+ },
321
+ "node_modules/finalhandler": {
322
+ "version": "2.1.0",
323
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
324
+ "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
325
+ "license": "MIT",
326
+ "dependencies": {
327
+ "debug": "^4.4.0",
328
+ "encodeurl": "^2.0.0",
329
+ "escape-html": "^1.0.3",
330
+ "on-finished": "^2.4.1",
331
+ "parseurl": "^1.3.3",
332
+ "statuses": "^2.0.1"
333
+ },
334
+ "engines": {
335
+ "node": ">= 0.8"
336
+ }
337
+ },
338
+ "node_modules/forwarded": {
339
+ "version": "0.2.0",
340
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
341
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
342
+ "license": "MIT",
343
+ "engines": {
344
+ "node": ">= 0.6"
345
+ }
346
+ },
347
+ "node_modules/fresh": {
348
+ "version": "2.0.0",
349
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
350
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
351
+ "license": "MIT",
352
+ "engines": {
353
+ "node": ">= 0.8"
354
+ }
355
+ },
356
+ "node_modules/function-bind": {
357
+ "version": "1.1.2",
358
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
359
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
360
+ "license": "MIT",
361
+ "funding": {
362
+ "url": "https://github.com/sponsors/ljharb"
363
+ }
364
+ },
365
+ "node_modules/get-intrinsic": {
366
+ "version": "1.3.0",
367
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
368
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
369
+ "license": "MIT",
370
+ "dependencies": {
371
+ "call-bind-apply-helpers": "^1.0.2",
372
+ "es-define-property": "^1.0.1",
373
+ "es-errors": "^1.3.0",
374
+ "es-object-atoms": "^1.1.1",
375
+ "function-bind": "^1.1.2",
376
+ "get-proto": "^1.0.1",
377
+ "gopd": "^1.2.0",
378
+ "has-symbols": "^1.1.0",
379
+ "hasown": "^2.0.2",
380
+ "math-intrinsics": "^1.1.0"
381
+ },
382
+ "engines": {
383
+ "node": ">= 0.4"
384
+ },
385
+ "funding": {
386
+ "url": "https://github.com/sponsors/ljharb"
387
+ }
388
+ },
389
+ "node_modules/get-proto": {
390
+ "version": "1.0.1",
391
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
392
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
393
+ "license": "MIT",
394
+ "dependencies": {
395
+ "dunder-proto": "^1.0.1",
396
+ "es-object-atoms": "^1.0.0"
397
+ },
398
+ "engines": {
399
+ "node": ">= 0.4"
400
+ }
401
+ },
402
+ "node_modules/gopd": {
403
+ "version": "1.2.0",
404
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
405
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
406
+ "license": "MIT",
407
+ "engines": {
408
+ "node": ">= 0.4"
409
+ },
410
+ "funding": {
411
+ "url": "https://github.com/sponsors/ljharb"
412
+ }
413
+ },
414
+ "node_modules/has-symbols": {
415
+ "version": "1.1.0",
416
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
417
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
418
+ "license": "MIT",
419
+ "engines": {
420
+ "node": ">= 0.4"
421
+ },
422
+ "funding": {
423
+ "url": "https://github.com/sponsors/ljharb"
424
+ }
425
+ },
426
+ "node_modules/hasown": {
427
+ "version": "2.0.2",
428
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
429
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
430
+ "license": "MIT",
431
+ "dependencies": {
432
+ "function-bind": "^1.1.2"
433
+ },
434
+ "engines": {
435
+ "node": ">= 0.4"
436
+ }
437
+ },
438
+ "node_modules/http-errors": {
439
+ "version": "2.0.0",
440
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
441
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
442
+ "license": "MIT",
443
+ "dependencies": {
444
+ "depd": "2.0.0",
445
+ "inherits": "2.0.4",
446
+ "setprototypeof": "1.2.0",
447
+ "statuses": "2.0.1",
448
+ "toidentifier": "1.0.1"
449
+ },
450
+ "engines": {
451
+ "node": ">= 0.8"
452
+ }
453
+ },
454
+ "node_modules/http-errors/node_modules/statuses": {
455
+ "version": "2.0.1",
456
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
457
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
458
+ "license": "MIT",
459
+ "engines": {
460
+ "node": ">= 0.8"
461
+ }
462
+ },
463
+ "node_modules/iconv-lite": {
464
+ "version": "0.6.3",
465
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
466
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
467
+ "license": "MIT",
468
+ "dependencies": {
469
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
470
+ },
471
+ "engines": {
472
+ "node": ">=0.10.0"
473
+ }
474
+ },
475
+ "node_modules/inherits": {
476
+ "version": "2.0.4",
477
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
478
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
479
+ "license": "ISC"
480
+ },
481
+ "node_modules/ipaddr.js": {
482
+ "version": "1.9.1",
483
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
484
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
485
+ "license": "MIT",
486
+ "engines": {
487
+ "node": ">= 0.10"
488
+ }
489
+ },
490
+ "node_modules/is-promise": {
491
+ "version": "4.0.0",
492
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
493
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
494
+ "license": "MIT"
495
+ },
496
+ "node_modules/math-intrinsics": {
497
+ "version": "1.1.0",
498
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
499
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
500
+ "license": "MIT",
501
+ "engines": {
502
+ "node": ">= 0.4"
503
+ }
504
+ },
505
+ "node_modules/media-typer": {
506
+ "version": "1.1.0",
507
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
508
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
509
+ "license": "MIT",
510
+ "engines": {
511
+ "node": ">= 0.8"
512
+ }
513
+ },
514
+ "node_modules/merge-descriptors": {
515
+ "version": "2.0.0",
516
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
517
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
518
+ "license": "MIT",
519
+ "engines": {
520
+ "node": ">=18"
521
+ },
522
+ "funding": {
523
+ "url": "https://github.com/sponsors/sindresorhus"
524
+ }
525
+ },
526
+ "node_modules/mime-db": {
527
+ "version": "1.54.0",
528
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
529
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
530
+ "license": "MIT",
531
+ "engines": {
532
+ "node": ">= 0.6"
533
+ }
534
+ },
535
+ "node_modules/mime-types": {
536
+ "version": "3.0.1",
537
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
538
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
539
+ "license": "MIT",
540
+ "dependencies": {
541
+ "mime-db": "^1.54.0"
542
+ },
543
+ "engines": {
544
+ "node": ">= 0.6"
545
+ }
546
+ },
547
+ "node_modules/minimist": {
548
+ "version": "1.2.8",
549
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
550
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
551
+ "license": "MIT",
552
+ "funding": {
553
+ "url": "https://github.com/sponsors/ljharb"
554
+ }
555
+ },
556
+ "node_modules/mkdirp": {
557
+ "version": "0.5.6",
558
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
559
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
560
+ "license": "MIT",
561
+ "dependencies": {
562
+ "minimist": "^1.2.6"
563
+ },
564
+ "bin": {
565
+ "mkdirp": "bin/cmd.js"
566
+ }
567
+ },
568
+ "node_modules/ms": {
569
+ "version": "2.1.3",
570
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
571
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
572
+ "license": "MIT"
573
+ },
574
+ "node_modules/multer": {
575
+ "version": "2.0.2",
576
+ "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
577
+ "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==",
578
+ "license": "MIT",
579
+ "dependencies": {
580
+ "append-field": "^1.0.0",
581
+ "busboy": "^1.6.0",
582
+ "concat-stream": "^2.0.0",
583
+ "mkdirp": "^0.5.6",
584
+ "object-assign": "^4.1.1",
585
+ "type-is": "^1.6.18",
586
+ "xtend": "^4.0.2"
587
+ },
588
+ "engines": {
589
+ "node": ">= 10.16.0"
590
+ }
591
+ },
592
+ "node_modules/multer/node_modules/media-typer": {
593
+ "version": "0.3.0",
594
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
595
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
596
+ "license": "MIT",
597
+ "engines": {
598
+ "node": ">= 0.6"
599
+ }
600
+ },
601
+ "node_modules/multer/node_modules/mime-db": {
602
+ "version": "1.52.0",
603
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
604
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
605
+ "license": "MIT",
606
+ "engines": {
607
+ "node": ">= 0.6"
608
+ }
609
+ },
610
+ "node_modules/multer/node_modules/mime-types": {
611
+ "version": "2.1.35",
612
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
613
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
614
+ "license": "MIT",
615
+ "dependencies": {
616
+ "mime-db": "1.52.0"
617
+ },
618
+ "engines": {
619
+ "node": ">= 0.6"
620
+ }
621
+ },
622
+ "node_modules/multer/node_modules/type-is": {
623
+ "version": "1.6.18",
624
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
625
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
626
+ "license": "MIT",
627
+ "dependencies": {
628
+ "media-typer": "0.3.0",
629
+ "mime-types": "~2.1.24"
630
+ },
631
+ "engines": {
632
+ "node": ">= 0.6"
633
+ }
634
+ },
635
+ "node_modules/negotiator": {
636
+ "version": "1.0.0",
637
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
638
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
639
+ "license": "MIT",
640
+ "engines": {
641
+ "node": ">= 0.6"
642
+ }
643
+ },
644
+ "node_modules/node-fetch": {
645
+ "version": "2.7.0",
646
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
647
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
648
+ "license": "MIT",
649
+ "dependencies": {
650
+ "whatwg-url": "^5.0.0"
651
+ },
652
+ "engines": {
653
+ "node": "4.x || >=6.0.0"
654
+ },
655
+ "peerDependencies": {
656
+ "encoding": "^0.1.0"
657
+ },
658
+ "peerDependenciesMeta": {
659
+ "encoding": {
660
+ "optional": true
661
+ }
662
+ }
663
+ },
664
+ "node_modules/object-assign": {
665
+ "version": "4.1.1",
666
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
667
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
668
+ "license": "MIT",
669
+ "engines": {
670
+ "node": ">=0.10.0"
671
+ }
672
+ },
673
+ "node_modules/object-inspect": {
674
+ "version": "1.13.4",
675
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
676
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
677
+ "license": "MIT",
678
+ "engines": {
679
+ "node": ">= 0.4"
680
+ },
681
+ "funding": {
682
+ "url": "https://github.com/sponsors/ljharb"
683
+ }
684
+ },
685
+ "node_modules/on-finished": {
686
+ "version": "2.4.1",
687
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
688
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
689
+ "license": "MIT",
690
+ "dependencies": {
691
+ "ee-first": "1.1.1"
692
+ },
693
+ "engines": {
694
+ "node": ">= 0.8"
695
+ }
696
+ },
697
+ "node_modules/once": {
698
+ "version": "1.4.0",
699
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
700
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
701
+ "license": "ISC",
702
+ "dependencies": {
703
+ "wrappy": "1"
704
+ }
705
+ },
706
+ "node_modules/parseurl": {
707
+ "version": "1.3.3",
708
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
709
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
710
+ "license": "MIT",
711
+ "engines": {
712
+ "node": ">= 0.8"
713
+ }
714
+ },
715
+ "node_modules/path-to-regexp": {
716
+ "version": "8.2.0",
717
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
718
+ "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
719
+ "license": "MIT",
720
+ "engines": {
721
+ "node": ">=16"
722
+ }
723
+ },
724
+ "node_modules/proxy-addr": {
725
+ "version": "2.0.7",
726
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
727
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
728
+ "license": "MIT",
729
+ "dependencies": {
730
+ "forwarded": "0.2.0",
731
+ "ipaddr.js": "1.9.1"
732
+ },
733
+ "engines": {
734
+ "node": ">= 0.10"
735
+ }
736
+ },
737
+ "node_modules/qs": {
738
+ "version": "6.14.0",
739
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
740
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
741
+ "license": "BSD-3-Clause",
742
+ "dependencies": {
743
+ "side-channel": "^1.1.0"
744
+ },
745
+ "engines": {
746
+ "node": ">=0.6"
747
+ },
748
+ "funding": {
749
+ "url": "https://github.com/sponsors/ljharb"
750
+ }
751
+ },
752
+ "node_modules/range-parser": {
753
+ "version": "1.2.1",
754
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
755
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
756
+ "license": "MIT",
757
+ "engines": {
758
+ "node": ">= 0.6"
759
+ }
760
+ },
761
+ "node_modules/raw-body": {
762
+ "version": "3.0.0",
763
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
764
+ "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
765
+ "license": "MIT",
766
+ "dependencies": {
767
+ "bytes": "3.1.2",
768
+ "http-errors": "2.0.0",
769
+ "iconv-lite": "0.6.3",
770
+ "unpipe": "1.0.0"
771
+ },
772
+ "engines": {
773
+ "node": ">= 0.8"
774
+ }
775
+ },
776
+ "node_modules/readable-stream": {
777
+ "version": "3.6.2",
778
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
779
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
780
+ "license": "MIT",
781
+ "dependencies": {
782
+ "inherits": "^2.0.3",
783
+ "string_decoder": "^1.1.1",
784
+ "util-deprecate": "^1.0.1"
785
+ },
786
+ "engines": {
787
+ "node": ">= 6"
788
+ }
789
+ },
790
+ "node_modules/router": {
791
+ "version": "2.2.0",
792
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
793
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
794
+ "license": "MIT",
795
+ "dependencies": {
796
+ "debug": "^4.4.0",
797
+ "depd": "^2.0.0",
798
+ "is-promise": "^4.0.0",
799
+ "parseurl": "^1.3.3",
800
+ "path-to-regexp": "^8.0.0"
801
+ },
802
+ "engines": {
803
+ "node": ">= 18"
804
+ }
805
+ },
806
+ "node_modules/safe-buffer": {
807
+ "version": "5.2.1",
808
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
809
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
810
+ "funding": [
811
+ {
812
+ "type": "github",
813
+ "url": "https://github.com/sponsors/feross"
814
+ },
815
+ {
816
+ "type": "patreon",
817
+ "url": "https://www.patreon.com/feross"
818
+ },
819
+ {
820
+ "type": "consulting",
821
+ "url": "https://feross.org/support"
822
+ }
823
+ ],
824
+ "license": "MIT"
825
+ },
826
+ "node_modules/safer-buffer": {
827
+ "version": "2.1.2",
828
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
829
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
830
+ "license": "MIT"
831
+ },
832
+ "node_modules/send": {
833
+ "version": "1.2.0",
834
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
835
+ "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
836
+ "license": "MIT",
837
+ "dependencies": {
838
+ "debug": "^4.3.5",
839
+ "encodeurl": "^2.0.0",
840
+ "escape-html": "^1.0.3",
841
+ "etag": "^1.8.1",
842
+ "fresh": "^2.0.0",
843
+ "http-errors": "^2.0.0",
844
+ "mime-types": "^3.0.1",
845
+ "ms": "^2.1.3",
846
+ "on-finished": "^2.4.1",
847
+ "range-parser": "^1.2.1",
848
+ "statuses": "^2.0.1"
849
+ },
850
+ "engines": {
851
+ "node": ">= 18"
852
+ }
853
+ },
854
+ "node_modules/serve-static": {
855
+ "version": "2.2.0",
856
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
857
+ "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
858
+ "license": "MIT",
859
+ "dependencies": {
860
+ "encodeurl": "^2.0.0",
861
+ "escape-html": "^1.0.3",
862
+ "parseurl": "^1.3.3",
863
+ "send": "^1.2.0"
864
+ },
865
+ "engines": {
866
+ "node": ">= 18"
867
+ }
868
+ },
869
+ "node_modules/setprototypeof": {
870
+ "version": "1.2.0",
871
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
872
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
873
+ "license": "ISC"
874
+ },
875
+ "node_modules/side-channel": {
876
+ "version": "1.1.0",
877
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
878
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
879
+ "license": "MIT",
880
+ "dependencies": {
881
+ "es-errors": "^1.3.0",
882
+ "object-inspect": "^1.13.3",
883
+ "side-channel-list": "^1.0.0",
884
+ "side-channel-map": "^1.0.1",
885
+ "side-channel-weakmap": "^1.0.2"
886
+ },
887
+ "engines": {
888
+ "node": ">= 0.4"
889
+ },
890
+ "funding": {
891
+ "url": "https://github.com/sponsors/ljharb"
892
+ }
893
+ },
894
+ "node_modules/side-channel-list": {
895
+ "version": "1.0.0",
896
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
897
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
898
+ "license": "MIT",
899
+ "dependencies": {
900
+ "es-errors": "^1.3.0",
901
+ "object-inspect": "^1.13.3"
902
+ },
903
+ "engines": {
904
+ "node": ">= 0.4"
905
+ },
906
+ "funding": {
907
+ "url": "https://github.com/sponsors/ljharb"
908
+ }
909
+ },
910
+ "node_modules/side-channel-map": {
911
+ "version": "1.0.1",
912
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
913
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
914
+ "license": "MIT",
915
+ "dependencies": {
916
+ "call-bound": "^1.0.2",
917
+ "es-errors": "^1.3.0",
918
+ "get-intrinsic": "^1.2.5",
919
+ "object-inspect": "^1.13.3"
920
+ },
921
+ "engines": {
922
+ "node": ">= 0.4"
923
+ },
924
+ "funding": {
925
+ "url": "https://github.com/sponsors/ljharb"
926
+ }
927
+ },
928
+ "node_modules/side-channel-weakmap": {
929
+ "version": "1.0.2",
930
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
931
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
932
+ "license": "MIT",
933
+ "dependencies": {
934
+ "call-bound": "^1.0.2",
935
+ "es-errors": "^1.3.0",
936
+ "get-intrinsic": "^1.2.5",
937
+ "object-inspect": "^1.13.3",
938
+ "side-channel-map": "^1.0.1"
939
+ },
940
+ "engines": {
941
+ "node": ">= 0.4"
942
+ },
943
+ "funding": {
944
+ "url": "https://github.com/sponsors/ljharb"
945
+ }
946
+ },
947
+ "node_modules/statuses": {
948
+ "version": "2.0.2",
949
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
950
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
951
+ "license": "MIT",
952
+ "engines": {
953
+ "node": ">= 0.8"
954
+ }
955
+ },
956
+ "node_modules/streamsearch": {
957
+ "version": "1.1.0",
958
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
959
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
960
+ "engines": {
961
+ "node": ">=10.0.0"
962
+ }
963
+ },
964
+ "node_modules/string_decoder": {
965
+ "version": "1.3.0",
966
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
967
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
968
+ "license": "MIT",
969
+ "dependencies": {
970
+ "safe-buffer": "~5.2.0"
971
+ }
972
+ },
973
+ "node_modules/toidentifier": {
974
+ "version": "1.0.1",
975
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
976
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
977
+ "license": "MIT",
978
+ "engines": {
979
+ "node": ">=0.6"
980
+ }
981
+ },
982
+ "node_modules/tr46": {
983
+ "version": "0.0.3",
984
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
985
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
986
+ "license": "MIT"
987
+ },
988
+ "node_modules/type-is": {
989
+ "version": "2.0.1",
990
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
991
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
992
+ "license": "MIT",
993
+ "dependencies": {
994
+ "content-type": "^1.0.5",
995
+ "media-typer": "^1.1.0",
996
+ "mime-types": "^3.0.0"
997
+ },
998
+ "engines": {
999
+ "node": ">= 0.6"
1000
+ }
1001
+ },
1002
+ "node_modules/typedarray": {
1003
+ "version": "0.0.6",
1004
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
1005
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
1006
+ "license": "MIT"
1007
+ },
1008
+ "node_modules/unpipe": {
1009
+ "version": "1.0.0",
1010
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1011
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1012
+ "license": "MIT",
1013
+ "engines": {
1014
+ "node": ">= 0.8"
1015
+ }
1016
+ },
1017
+ "node_modules/util-deprecate": {
1018
+ "version": "1.0.2",
1019
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
1020
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
1021
+ "license": "MIT"
1022
+ },
1023
+ "node_modules/vary": {
1024
+ "version": "1.1.2",
1025
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1026
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1027
+ "license": "MIT",
1028
+ "engines": {
1029
+ "node": ">= 0.8"
1030
+ }
1031
+ },
1032
+ "node_modules/webidl-conversions": {
1033
+ "version": "3.0.1",
1034
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
1035
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
1036
+ "license": "BSD-2-Clause"
1037
+ },
1038
+ "node_modules/whatwg-url": {
1039
+ "version": "5.0.0",
1040
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
1041
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
1042
+ "license": "MIT",
1043
+ "dependencies": {
1044
+ "tr46": "~0.0.3",
1045
+ "webidl-conversions": "^3.0.0"
1046
+ }
1047
+ },
1048
+ "node_modules/wrappy": {
1049
+ "version": "1.0.2",
1050
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
1051
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
1052
+ "license": "ISC"
1053
+ },
1054
+ "node_modules/xtend": {
1055
+ "version": "4.0.2",
1056
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
1057
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
1058
+ "license": "MIT",
1059
+ "engines": {
1060
+ "node": ">=0.4"
1061
+ }
1062
+ }
1063
+ }
1064
+ }
package.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "ytdlp",
3
+ "version": "1.0.0",
4
+ "main": "app.js",
5
+ "scripts": {
6
+ "start": "node app.js",
7
+ "dev": "node app.js",
8
+ "test": "echo \"Error: no test specified\" && exit 1"
9
+ },
10
+ "keywords": [],
11
+ "author": "",
12
+ "license": "ISC",
13
+ "description": "",
14
+ "dependencies": {
15
+ "cors": "^2.8.5",
16
+ "express": "^5.1.0",
17
+ "multer": "^2.0.2",
18
+ "node-fetch": "^2.7.0"
19
+ }
20
+ }