devusman commited on
Commit
c4278f8
·
1 Parent(s): 039bf25

ready for deployment

Browse files
Files changed (5) hide show
  1. Dockerfile +27 -0
  2. app.py +348 -0
  3. cookies.txt +15 -0
  4. requirements.txt +6 -0
  5. templates/index.html +310 -0
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use a lightweight Python image
2
+ FROM python:3.11-slim
3
+
4
+ # Install ffmpeg (needed by yt-dlp)
5
+ RUN apt-get update && apt-get install -y \
6
+ ffmpeg \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ # Set working directory
10
+ WORKDIR /app
11
+
12
+ # Copy requirements first to leverage Docker cache
13
+ COPY requirements.txt .
14
+
15
+ # Install dependencies
16
+ # Ensure gunicorn and gevent are included in your requirements.txt
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Copy project files
20
+ COPY . .
21
+
22
+ # Expose the port Gunicorn will run on. Render's default is 10000.
23
+ EXPOSE 10000
24
+
25
+ # Start the Flask app using Gunicorn with the gevent worker and a longer timeout
26
+ # This prevents the worker from timing out during long downloads.
27
+ CMD ["gunicorn", "--worker-class", "gevent", "--timeout", "3600", "--bind", "0.0.0.0:10000", "app:app"]
app.py ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import time
4
+ import logging
5
+ import tempfile
6
+ import yt_dlp
7
+ from flask import Flask, Response, render_template, request
8
+ from werkzeug.utils import secure_filename
9
+ from threading import Thread, Lock
10
+ import uuid
11
+
12
+ app = Flask(__name__)
13
+
14
+ # --- Configuration ---
15
+
16
+ def get_temp_download_path():
17
+ render_disk_path = os.environ.get('RENDER_DISK_PATH')
18
+ if render_disk_path:
19
+ base_path = render_disk_path
20
+ else:
21
+ base_path = tempfile.gettempdir()
22
+
23
+ temp_folder = os.path.join(base_path, 'yt_temp_downloads')
24
+ return temp_folder
25
+
26
+ DOWNLOAD_FOLDER = get_temp_download_path()
27
+ COOKIE_FILE = "cookies.txt"
28
+
29
+ logging.basicConfig(level=logging.INFO)
30
+ os.makedirs(DOWNLOAD_FOLDER, exist_ok=True)
31
+
32
+ # Progress storage with thread safety
33
+ progress_data = {}
34
+ progress_lock = Lock()
35
+
36
+ def update_progress(task_id, status, progress=0, **kwargs):
37
+ """Update progress data thread-safely"""
38
+ with progress_lock:
39
+ progress_data[task_id] = {
40
+ 'status': status,
41
+ 'progress': float(progress),
42
+ 'timestamp': time.time(),
43
+ **kwargs
44
+ }
45
+ app.logger.info(f"PROGRESS: {task_id} -> {status} ({progress}%)")
46
+
47
+ # Global variable to hold current task_id for progress hook
48
+ current_download_task = None
49
+
50
+ def create_progress_hook(task_id):
51
+ """Create a progress hook that captures the task_id in closure"""
52
+ def progress_hook(d):
53
+ try:
54
+ if d['status'] == 'downloading':
55
+ total_bytes = d.get('total_bytes') or d.get('total_bytes_est', 0)
56
+ downloaded_bytes = d.get('downloaded_bytes', 0)
57
+ speed = d.get('speed', 0)
58
+ eta = d.get('eta', 0)
59
+
60
+ if total_bytes > 0:
61
+ percent = (downloaded_bytes / total_bytes) * 100
62
+ # Don't let it reach 100% until actually finished
63
+ percent = min(99.9, percent)
64
+ else:
65
+ # If we don't have total size, estimate based on downloaded amount
66
+ # This is a fallback for cases where total size is unknown
67
+ percent = min(50, downloaded_bytes / (1024 * 1024)) # Rough estimate
68
+
69
+ speed_mbps = (speed / (1024 * 1024)) if speed else 0
70
+
71
+ update_progress(
72
+ task_id,
73
+ 'downloading',
74
+ percent,
75
+ eta=int(eta) if eta else 0,
76
+ speed=f"{speed_mbps:.2f} MB/s",
77
+ downloaded_mb=downloaded_bytes / (1024 * 1024),
78
+ total_mb=total_bytes / (1024 * 1024) if total_bytes else 0
79
+ )
80
+
81
+ elif d['status'] == 'finished':
82
+ update_progress(task_id, 'processing', 100)
83
+
84
+ except Exception as e:
85
+ app.logger.error(f"Progress hook error for {task_id}: {e}")
86
+
87
+ return progress_hook
88
+
89
+ def download_worker(url, format_choice, task_id):
90
+ """Download worker function"""
91
+ global current_download_task
92
+ current_download_task = task_id
93
+
94
+ try:
95
+ # Step 1: Initialize
96
+ update_progress(task_id, 'initializing', 5)
97
+ time.sleep(0.5)
98
+
99
+ # Step 2: Get video info
100
+ update_progress(task_id, 'fetching_info', 10)
101
+ info_opts = {
102
+ 'quiet': True,
103
+ 'no_warnings': True,
104
+ 'cookiefile': COOKIE_FILE if os.path.exists(COOKIE_FILE) else None
105
+ }
106
+
107
+ with yt_dlp.YoutubeDL(info_opts) as ydl:
108
+ info = ydl.extract_info(url, download=False)
109
+ clean_title = secure_filename(info.get('title', 'video'))
110
+ app.logger.info(f"Video title: {clean_title}")
111
+
112
+ # Step 3: Prepare download
113
+ update_progress(task_id, 'preparing', 15)
114
+ time.sleep(0.5)
115
+
116
+ # Configure download options
117
+ timestamp = int(time.time())
118
+ audio_formats = {"mp3", "m4a", "webm", "aac", "flac", "opus", "ogg", "wav"}
119
+
120
+ if format_choice in audio_formats:
121
+ unique_name = f"{clean_title}_{timestamp}"
122
+ final_filename = f"{unique_name}.{format_choice}"
123
+ ydl_opts = {
124
+ 'format': 'bestaudio/best',
125
+ 'postprocessors': [{
126
+ 'key': 'FFmpegExtractAudio',
127
+ 'preferredcodec': format_choice,
128
+ 'preferredquality': '192',
129
+ }]
130
+ }
131
+ elif format_choice in ["1080", "720", "480", "360", "1440"]:
132
+ res = int(format_choice)
133
+ unique_name = f"{clean_title}_{res}p_{timestamp}"
134
+ final_filename = f"{unique_name}.mp4"
135
+ ydl_opts = {
136
+ 'format': f'bestvideo[height<={res}]+bestaudio/best[height<={res}]',
137
+ 'merge_output_format': 'mp4'
138
+ }
139
+ else:
140
+ raise ValueError(f"Invalid format: {format_choice}")
141
+
142
+ # Create progress hook with task_id captured in closure
143
+ progress_hook = create_progress_hook(task_id)
144
+
145
+ # Set common options
146
+ ydl_opts.update({
147
+ 'outtmpl': os.path.join(DOWNLOAD_FOLDER, f"{unique_name}.%(ext)s"),
148
+ 'progress_hooks': [progress_hook],
149
+ 'quiet': True,
150
+ 'no_warnings': True,
151
+ 'cookiefile': COOKIE_FILE if os.path.exists(COOKIE_FILE) else None
152
+ })
153
+
154
+ expected_path = os.path.join(DOWNLOAD_FOLDER, final_filename)
155
+
156
+ # Step 4: Start download
157
+ update_progress(task_id, 'starting_download', 20)
158
+ time.sleep(0.5)
159
+
160
+ app.logger.info(f"Starting yt-dlp download for {task_id}")
161
+
162
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
163
+ ydl.download([url])
164
+
165
+ app.logger.info(f"yt-dlp download completed for {task_id}")
166
+
167
+ # Step 5: Find the downloaded file
168
+ actual_filename = final_filename
169
+ if not os.path.exists(expected_path):
170
+ # Look for any file with the base name
171
+ base_name = unique_name
172
+ found_files = []
173
+
174
+ for filename in os.listdir(DOWNLOAD_FOLDER):
175
+ if filename.startswith(base_name) and not filename.endswith('.part'):
176
+ found_files.append(filename)
177
+
178
+ if found_files:
179
+ # Use the most recent file
180
+ found_files.sort(key=lambda x: os.path.getctime(os.path.join(DOWNLOAD_FOLDER, x)), reverse=True)
181
+ actual_filename = found_files[0]
182
+ expected_path = os.path.join(DOWNLOAD_FOLDER, actual_filename)
183
+ app.logger.info(f"Found downloaded file: {actual_filename}")
184
+
185
+ # Verify file exists and isn't empty
186
+ if not os.path.exists(expected_path):
187
+ raise FileNotFoundError(f"Download completed but file not found: {actual_filename}")
188
+
189
+ file_size = os.path.getsize(expected_path)
190
+ if file_size == 0:
191
+ raise ValueError("Downloaded file is empty")
192
+
193
+ app.logger.info(f"File verified: {actual_filename} ({file_size} bytes)")
194
+
195
+ # Step 6: Complete
196
+ update_progress(task_id, 'complete', 100, filename=actual_filename)
197
+
198
+ except Exception as e:
199
+ error_msg = str(e)
200
+ app.logger.error(f"Download error for {task_id}: {error_msg}")
201
+ update_progress(task_id, 'error', 0, message=error_msg)
202
+
203
+ # Cleanup on error
204
+ try:
205
+ if 'expected_path' in locals() and os.path.exists(expected_path):
206
+ os.remove(expected_path)
207
+ app.logger.info(f"Cleaned up failed download: {expected_path}")
208
+ except Exception as cleanup_error:
209
+ app.logger.error(f"Cleanup error: {cleanup_error}")
210
+
211
+ finally:
212
+ # Clear global task reference
213
+ if current_download_task == task_id:
214
+ current_download_task = None
215
+
216
+ @app.route('/')
217
+ def index():
218
+ return render_template('index.html')
219
+
220
+ @app.route('/stream-download', methods=['GET'])
221
+ def stream_download():
222
+ url = request.args.get('url')
223
+ format_choice = request.args.get('format')
224
+
225
+ if not url or not format_choice:
226
+ return Response(
227
+ json.dumps({"error": "Missing parameters"}),
228
+ status=400,
229
+ mimetype='application/json'
230
+ )
231
+
232
+ task_id = str(uuid.uuid4())
233
+ app.logger.info(f"New download request: {task_id} - {url} - {format_choice}")
234
+
235
+ # Initialize progress
236
+ update_progress(task_id, 'waiting', 0)
237
+
238
+ # Start download thread
239
+ thread = Thread(target=download_worker, args=(url, format_choice, task_id))
240
+ thread.daemon = True
241
+ thread.start()
242
+
243
+ def generate():
244
+ try:
245
+ start_time = time.time()
246
+ timeout = 1800 # 30 minutes
247
+
248
+ while True:
249
+ # Check timeout
250
+ if time.time() - start_time > timeout:
251
+ update_progress(task_id, 'error', 0, message='Download timeout')
252
+ break
253
+
254
+ # Get current progress
255
+ with progress_lock:
256
+ data = progress_data.get(task_id, {
257
+ 'status': 'waiting',
258
+ 'progress': 0,
259
+ 'timestamp': time.time()
260
+ })
261
+
262
+ # Send data
263
+ json_data = json.dumps(data)
264
+ yield f"data: {json_data}\n\n"
265
+
266
+ # Check if finished
267
+ if data.get('status') in ['complete', 'error']:
268
+ break
269
+
270
+ time.sleep(0.5)
271
+
272
+ except GeneratorExit:
273
+ app.logger.info(f"Client disconnected: {task_id}")
274
+ except Exception as e:
275
+ app.logger.error(f"Stream error for {task_id}: {e}")
276
+ finally:
277
+ # Cleanup after delay
278
+ def cleanup():
279
+ time.sleep(30)
280
+ with progress_lock:
281
+ if task_id in progress_data:
282
+ del progress_data[task_id]
283
+ app.logger.info(f"Cleaned up progress data for {task_id}")
284
+
285
+ Thread(target=cleanup, daemon=True).start()
286
+
287
+ response = Response(generate(), mimetype='text/event-stream')
288
+ response.headers.update({
289
+ 'Cache-Control': 'no-cache',
290
+ 'Connection': 'keep-alive',
291
+ 'Access-Control-Allow-Origin': '*',
292
+ 'X-Accel-Buffering': 'no'
293
+ })
294
+
295
+ return response
296
+
297
+ @app.route('/download-file/<filename>')
298
+ def download_file(filename):
299
+ safe_folder = os.path.abspath(DOWNLOAD_FOLDER)
300
+ filepath = os.path.join(safe_folder, filename)
301
+
302
+ # Security check
303
+ if not os.path.abspath(filepath).startswith(safe_folder):
304
+ return "Forbidden", 403
305
+
306
+ if not os.path.exists(filepath):
307
+ return "File not found", 404
308
+
309
+ def generate_and_cleanup():
310
+ try:
311
+ app.logger.info(f"Serving file: {filename}")
312
+ with open(filepath, 'rb') as f:
313
+ while True:
314
+ chunk = f.read(8192)
315
+ if not chunk:
316
+ break
317
+ yield chunk
318
+ finally:
319
+ # Remove file after download
320
+ def remove_file():
321
+ time.sleep(2)
322
+ try:
323
+ if os.path.exists(filepath):
324
+ os.remove(filepath)
325
+ app.logger.info(f"Cleaned up file: {filename}")
326
+ except Exception as e:
327
+ app.logger.error(f"Error removing file {filename}: {e}")
328
+
329
+ Thread(target=remove_file, daemon=True).start()
330
+
331
+ return Response(
332
+ generate_and_cleanup(),
333
+ headers={
334
+ 'Content-Disposition': f'attachment; filename="{filename}"',
335
+ 'Content-Type': 'application/octet-stream'
336
+ }
337
+ )
338
+
339
+ @app.route('/health')
340
+ def health_check():
341
+ return {
342
+ "status": "healthy",
343
+ "active_downloads": len(progress_data),
344
+ "current_task": current_download_task
345
+ }
346
+
347
+ if __name__ == "__main__":
348
+ app.run(debug=True, threaded=True, host='0.0.0.0', port=5000)
cookies.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Netscape HTTP Cookie File
2
+ # This file is generated by yt-dlp. Do not edit.
3
+
4
+ .vimeo.com TRUE / TRUE 2073814202 has_uploaded 1
5
+ .vimeo.com TRUE / TRUE 2073814202 language en
6
+ .vimeo.com TRUE / TRUE 1793014133 _ga GA1.2.1668146364.1758217509
7
+ .vimeo.com TRUE / TRUE 1758540533 _gid GA1.2.1612255520.1758454116
8
+ .vimeo.com TRUE / TRUE 1761046202 vimeo OHDdZDLXZ4eMVHLZetdedLLPMNVHLZetdedD4LMxH3cdXcSDc%2CDZDZLNeNLtPPXdNDXX%2CdN%2CBDXZdcXPdM%2Cu9Y_%2CBHLMIHX3XZNt%2CtBtPed%2CLea%2C%2CtBSSPN3Ne%2C3D34Za4ZXeXc4tLdece43D3SBN4dDLeDctL
9
+ .vimeo.com TRUE / TRUE 1758456002 __cf_bm AiUWyZ8xP90NXJIWLGw3uMotSc1YBk3pt40v1Tfz_wk-1758454202-1.0.1.1-Z1X7MnPMKmDwNbqjqwLVQcbQ9um8lSG051Kn8g5ardtNa5PTJCg8Q7IJrdCwaVqA
10
+ .vimeo.com TRUE / TRUE 1793014120 joined_with_existing_account 1
11
+ .vimeo.com TRUE / TRUE 1759058920 vimeo_cart %7B%22stock%22%3A%7B%22store%22%3A%22stock%22%2C%22version%22%3A1%2C%22quantities%22%3A%5B%5D%2C%22items%22%3A%5B%5D%2C%22attributes%22%3A%5B%5D%2C%22currency%22%3A%22USD%22%2C%22items_sorted_by_index%22%3A%5B%5D%2C%22items_count%22%3A0%7D%7D
12
+ .vimeo.com TRUE / TRUE 2073814202 vuid pl1830665756.1005461139
13
+ .vimeo.com TRUE / TRUE 0 _cfuvid oIXli2u39Hrkua_Kvsi6nW8D6zi6XxCs_Ur5_S6hFWw-1758454202238-0.0.1.1-604800000
14
+ .vimeo.com TRUE / TRUE 1758457720 auth_xsrft 825d15e45c76dc9ab63cfdb1a9930938ec4d7fc0
15
+ vimeo.com FALSE / TRUE 1758455909 builderSessionId 8ba56e98cc724a928dc442bf9beeee26
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ yt-dlp
2
+ flask
3
+ brotli
4
+ certifi
5
+ gunicorn
6
+ gevent
templates/index.html ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Vimeo Downloader</title>
7
+ <style>
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
10
+ Helvetica, Arial, sans-serif;
11
+ background-color: #f0f2f5;
12
+ display: flex;
13
+ justify-content: center;
14
+ align-items: center;
15
+ min-height: 100vh;
16
+ margin: 0;
17
+ padding: 1rem;
18
+ box-sizing: border-box;
19
+ }
20
+ .container {
21
+ background: white;
22
+ padding: 2rem 2.5rem;
23
+ border-radius: 8px;
24
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
25
+ width: 100%;
26
+ max-width: 500px;
27
+ }
28
+ h1 {
29
+ font-size: 1.75rem;
30
+ color: #1c1e21;
31
+ margin: 0 0 1.5rem 0;
32
+ text-align: center;
33
+ }
34
+ .form-group {
35
+ margin-bottom: 1rem;
36
+ }
37
+ label {
38
+ display: block;
39
+ margin-bottom: 0.5rem;
40
+ font-weight: 600;
41
+ color: #606770;
42
+ }
43
+ input,
44
+ select,
45
+ button {
46
+ width: 100%;
47
+ padding: 0.75rem;
48
+ font-size: 1rem;
49
+ border-radius: 6px;
50
+ border: 1px solid #dddfe2;
51
+ box-sizing: border-box;
52
+ }
53
+ button {
54
+ background-color: #007bff;
55
+ color: white;
56
+ border: none;
57
+ cursor: pointer;
58
+ font-weight: bold;
59
+ transition: background-color 0.2s;
60
+ }
61
+ button:disabled {
62
+ background-color: #a0c9f8;
63
+ cursor: not-allowed;
64
+ }
65
+
66
+ #progress-container {
67
+ display: none;
68
+ margin-top: 1.5rem;
69
+ }
70
+ #progress-bar-wrapper {
71
+ width: 100%;
72
+ background-color: #e9ecef;
73
+ border-radius: 0.25rem;
74
+ overflow: hidden;
75
+ height: 30px;
76
+ border: 1px solid #ddd;
77
+ }
78
+ #progress-bar {
79
+ height: 100%;
80
+ background-color: #007bff;
81
+ color: white;
82
+ text-align: center;
83
+ line-height: 30px;
84
+ font-weight: bold;
85
+ transition: width 0.3s ease;
86
+ width: 0%;
87
+ min-width: 60px;
88
+ }
89
+ #status-text {
90
+ text-align: center;
91
+ margin-top: 0.75rem;
92
+ font-weight: 500;
93
+ color: #495057;
94
+ min-height: 20px;
95
+ }
96
+ #download-link-container {
97
+ text-align: center;
98
+ margin-top: 1rem;
99
+ }
100
+ #download-link-container a {
101
+ display: inline-block;
102
+ padding: 0.75rem 1.5rem;
103
+ background-color: #28a745;
104
+ color: white;
105
+ text-decoration: none;
106
+ border-radius: 6px;
107
+ font-weight: bold;
108
+ }
109
+ .debug {
110
+ font-size: 12px;
111
+ color: #666;
112
+ margin-top: 5px;
113
+ text-align: center;
114
+ }
115
+ </style>
116
+ </head>
117
+ <body>
118
+ <div class="container">
119
+ <h1>Vimeo Downloader</h1>
120
+ <form id="download-form">
121
+ <div class="form-group">
122
+ <label for="url">URL</label>
123
+ <input
124
+ type="text"
125
+ id="url"
126
+ name="url"
127
+ placeholder="Paste Vimeo URL here"
128
+ required
129
+ />
130
+ </div>
131
+ <div class="form-group">
132
+ <label for="format">Format</label>
133
+ <select id="format" name="format">
134
+ <optgroup label="Audio">
135
+ <option value="mp3" selected>MP3</option>
136
+ <option value="m4a">M4A</option>
137
+ <option value="flac">FLAC</option>
138
+ <option value="opus">OPUS</option>
139
+ <option value="wav">WAV</option>
140
+ </optgroup>
141
+ <optgroup label="Video">
142
+ <option value="1080">MP4 (1080p)</option>
143
+ <option value="720">MP4 (720p)</option>
144
+ <option value="480">MP4 (480p)</option>
145
+ <option value="360">MP4 (360p)</option>
146
+ <option value="1440">MP4 (1440p)</option>
147
+ </optgroup>
148
+ </select>
149
+ </div>
150
+ <button type="submit" id="download-button">Download</button>
151
+ </form>
152
+
153
+ <div id="progress-container">
154
+ <div id="progress-bar-wrapper">
155
+ <div id="progress-bar">0%</div>
156
+ </div>
157
+ <div id="status-text">Starting...</div>
158
+ <div id="debug-info" class="debug"></div>
159
+ <div id="download-link-container"></div>
160
+ </div>
161
+ </div>
162
+
163
+ <script>
164
+ const form = document.getElementById("download-form");
165
+ const button = document.getElementById("download-button");
166
+ const progressContainer = document.getElementById("progress-container");
167
+ const progressBar = document.getElementById("progress-bar");
168
+ const statusText = document.getElementById("status-text");
169
+ const debugInfo = document.getElementById("debug-info");
170
+ const downloadContainer = document.getElementById(
171
+ "download-link-container"
172
+ );
173
+
174
+ let eventSource;
175
+
176
+ form.addEventListener("submit", function (e) {
177
+ e.preventDefault();
178
+
179
+ // Reset UI
180
+ button.disabled = true;
181
+ button.textContent = "Downloading...";
182
+ progressContainer.style.display = "block";
183
+ downloadContainer.innerHTML = "";
184
+
185
+ // Reset progress bar
186
+ progressBar.style.width = "0%";
187
+ progressBar.textContent = "0%";
188
+ progressBar.style.backgroundColor = "#007bff";
189
+ statusText.textContent = "Connecting...";
190
+ debugInfo.textContent = "";
191
+
192
+ // Close existing connection
193
+ if (eventSource) {
194
+ eventSource.close();
195
+ }
196
+
197
+ // Build URL
198
+ const formData = new FormData(form);
199
+ const params = new URLSearchParams(formData);
200
+ const streamUrl = `/stream-download?${params}`;
201
+
202
+ console.log("Starting EventSource:", streamUrl);
203
+ eventSource = new EventSource(streamUrl);
204
+
205
+ eventSource.onopen = function (event) {
206
+ console.log("EventSource opened");
207
+ statusText.textContent = "Connected to server...";
208
+ };
209
+
210
+ eventSource.onmessage = function (event) {
211
+ try {
212
+ const data = JSON.parse(event.data);
213
+ console.log("Received:", data);
214
+
215
+ const progress = parseFloat(data.progress || 0);
216
+ const status = data.status || "unknown";
217
+
218
+ // Update debug info
219
+ debugInfo.textContent = `Status: ${status} | Progress: ${progress.toFixed(
220
+ 1
221
+ )}%`;
222
+
223
+ // Update progress bar
224
+ progressBar.style.width = `${progress}%`;
225
+ progressBar.textContent = `${progress.toFixed(1)}%`;
226
+
227
+ // Update status and colors
228
+ switch (status) {
229
+ case "waiting":
230
+ statusText.textContent = "Waiting for server...";
231
+ progressBar.style.backgroundColor = "#6c757d";
232
+ break;
233
+ case "initializing":
234
+ statusText.textContent = "Initializing download...";
235
+ progressBar.style.backgroundColor = "#007bff";
236
+ break;
237
+ case "fetching_info":
238
+ statusText.textContent = "Fetching video information...";
239
+ progressBar.style.backgroundColor = "#007bff";
240
+ break;
241
+ case "preparing":
242
+ statusText.textContent = "Preparing download...";
243
+ progressBar.style.backgroundColor = "#007bff";
244
+ break;
245
+ case "starting_download":
246
+ statusText.textContent = "Starting download...";
247
+ progressBar.style.backgroundColor = "#007bff";
248
+ break;
249
+ case "downloading":
250
+ const eta = data.eta || 0;
251
+ const speed = data.speed || "0 MB/s";
252
+ statusText.textContent = `Downloading... ETA: ${eta}s at ${speed}`;
253
+ progressBar.style.backgroundColor = "#007bff";
254
+ break;
255
+ case "processing":
256
+ statusText.textContent = "Processing file...";
257
+ progressBar.style.backgroundColor = "#ffc107";
258
+ progressBar.textContent = "Processing...";
259
+ break;
260
+ case "complete":
261
+ statusText.textContent = "Download Complete!";
262
+ progressBar.style.backgroundColor = "#28a745";
263
+ progressBar.textContent = "Done!";
264
+
265
+ if (data.filename) {
266
+ downloadContainer.innerHTML = `<a href="/download-file/${data.filename}" download>Click to Save File</a>`;
267
+ }
268
+
269
+ eventSource.close();
270
+ resetButton();
271
+ break;
272
+ case "error":
273
+ const errorMsg = data.message || "Download failed";
274
+ statusText.textContent = `Error: ${errorMsg}`;
275
+ progressBar.style.backgroundColor = "#dc3545";
276
+ progressBar.textContent = "Error!";
277
+
278
+ eventSource.close();
279
+ resetButton();
280
+ break;
281
+ }
282
+ } catch (err) {
283
+ console.error("Error parsing message:", err, event.data);
284
+ statusText.textContent = "Error processing server response";
285
+ resetButton();
286
+ }
287
+ };
288
+
289
+ eventSource.onerror = function (event) {
290
+ console.error("EventSource error:", event);
291
+ statusText.textContent = "Connection error. Please try again.";
292
+ eventSource.close();
293
+ resetButton();
294
+ };
295
+ });
296
+
297
+ function resetButton() {
298
+ button.disabled = false;
299
+ button.textContent = "Download";
300
+ }
301
+
302
+ // Cleanup on page unload
303
+ window.addEventListener("beforeunload", function () {
304
+ if (eventSource) {
305
+ eventSource.close();
306
+ }
307
+ });
308
+ </script>
309
+ </body>
310
+ </html>