OrbitMC commited on
Commit
0087505
·
verified ·
1 Parent(s): 582367b

Create app.pu

Browse files
Files changed (1) hide show
  1. app.pu +668 -0
app.pu ADDED
@@ -0,0 +1,668 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template_string, request, send_file, jsonify
2
+ import yt_dlp
3
+ import os
4
+ import uuid
5
+ import threading
6
+ import time
7
+ import re
8
+
9
+ app = Flask(__name__)
10
+
11
+ # Directory for downloads
12
+ DOWNLOAD_DIR = "/tmp/downloads"
13
+ os.makedirs(DOWNLOAD_DIR, exist_ok=True)
14
+
15
+ # HTML Template with Clean Responsive UI
16
+ HTML_TEMPLATE = """
17
+ <!DOCTYPE html>
18
+ <html lang="en">
19
+ <head>
20
+ <meta charset="UTF-8">
21
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
22
+ <title>🎬 Media Downloader - MP4 & MP3</title>
23
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
24
+ <style>
25
+ * {
26
+ margin: 0;
27
+ padding: 0;
28
+ box-sizing: border-box;
29
+ }
30
+
31
+ body {
32
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
33
+ background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%);
34
+ min-height: 100vh;
35
+ display: flex;
36
+ justify-content: center;
37
+ align-items: center;
38
+ padding: 20px;
39
+ }
40
+
41
+ .container {
42
+ background: rgba(255, 255, 255, 0.05);
43
+ backdrop-filter: blur(20px);
44
+ border-radius: 24px;
45
+ padding: 40px;
46
+ max-width: 650px;
47
+ width: 100%;
48
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
49
+ border: 1px solid rgba(255, 255, 255, 0.1);
50
+ }
51
+
52
+ .header {
53
+ text-align: center;
54
+ margin-bottom: 35px;
55
+ }
56
+
57
+ .logo {
58
+ font-size: 3.5rem;
59
+ margin-bottom: 15px;
60
+ animation: bounce 2s infinite;
61
+ }
62
+
63
+ @keyframes bounce {
64
+ 0%, 100% { transform: translateY(0); }
65
+ 50% { transform: translateY(-10px); }
66
+ }
67
+
68
+ h1 {
69
+ color: #fff;
70
+ font-size: 2rem;
71
+ font-weight: 700;
72
+ margin-bottom: 8px;
73
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
74
+ -webkit-background-clip: text;
75
+ -webkit-text-fill-color: transparent;
76
+ background-clip: text;
77
+ }
78
+
79
+ .subtitle {
80
+ color: rgba(255, 255, 255, 0.6);
81
+ font-size: 0.95rem;
82
+ font-weight: 300;
83
+ }
84
+
85
+ .input-section {
86
+ margin-bottom: 25px;
87
+ }
88
+
89
+ .input-wrapper {
90
+ position: relative;
91
+ }
92
+
93
+ .input-icon {
94
+ position: absolute;
95
+ left: 18px;
96
+ top: 50%;
97
+ transform: translateY(-50%);
98
+ font-size: 1.2rem;
99
+ }
100
+
101
+ input[type="text"] {
102
+ width: 100%;
103
+ padding: 18px 20px 18px 50px;
104
+ border: 2px solid rgba(255, 255, 255, 0.1);
105
+ border-radius: 16px;
106
+ background: rgba(255, 255, 255, 0.08);
107
+ color: #fff;
108
+ font-size: 1rem;
109
+ font-family: inherit;
110
+ transition: all 0.3s ease;
111
+ }
112
+
113
+ input[type="text"]:focus {
114
+ outline: none;
115
+ border-color: #667eea;
116
+ background: rgba(255, 255, 255, 0.12);
117
+ box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.2);
118
+ }
119
+
120
+ input[type="text"]::placeholder {
121
+ color: rgba(255, 255, 255, 0.4);
122
+ }
123
+
124
+ .btn-group {
125
+ display: grid;
126
+ grid-template-columns: 1fr 1fr;
127
+ gap: 15px;
128
+ margin-top: 25px;
129
+ }
130
+
131
+ .btn {
132
+ padding: 18px 28px;
133
+ border: none;
134
+ border-radius: 14px;
135
+ font-size: 1rem;
136
+ font-weight: 600;
137
+ font-family: inherit;
138
+ cursor: pointer;
139
+ transition: all 0.3s ease;
140
+ display: flex;
141
+ align-items: center;
142
+ justify-content: center;
143
+ gap: 10px;
144
+ position: relative;
145
+ overflow: hidden;
146
+ }
147
+
148
+ .btn::before {
149
+ content: '';
150
+ position: absolute;
151
+ top: 0;
152
+ left: -100%;
153
+ width: 100%;
154
+ height: 100%;
155
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
156
+ transition: left 0.5s ease;
157
+ }
158
+
159
+ .btn:hover::before {
160
+ left: 100%;
161
+ }
162
+
163
+ .btn-mp4 {
164
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
165
+ color: #fff;
166
+ }
167
+
168
+ .btn-mp3 {
169
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
170
+ color: #fff;
171
+ }
172
+
173
+ .btn:hover {
174
+ transform: translateY(-3px);
175
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
176
+ }
177
+
178
+ .btn:active {
179
+ transform: translateY(-1px);
180
+ }
181
+
182
+ .btn:disabled {
183
+ opacity: 0.6;
184
+ cursor: not-allowed;
185
+ transform: none !important;
186
+ }
187
+
188
+ .btn-icon {
189
+ font-size: 1.3rem;
190
+ }
191
+
192
+ .status {
193
+ margin-top: 25px;
194
+ padding: 18px 20px;
195
+ border-radius: 14px;
196
+ text-align: center;
197
+ font-weight: 500;
198
+ display: none;
199
+ animation: fadeIn 0.3s ease;
200
+ }
201
+
202
+ @keyframes fadeIn {
203
+ from { opacity: 0; transform: translateY(-10px); }
204
+ to { opacity: 1; transform: translateY(0); }
205
+ }
206
+
207
+ .status.loading {
208
+ display: flex;
209
+ align-items: center;
210
+ justify-content: center;
211
+ gap: 12px;
212
+ background: rgba(255, 193, 7, 0.15);
213
+ color: #ffc107;
214
+ border: 1px solid rgba(255, 193, 7, 0.3);
215
+ }
216
+
217
+ .status.success {
218
+ display: block;
219
+ background: rgba(76, 217, 100, 0.15);
220
+ color: #4cd964;
221
+ border: 1px solid rgba(76, 217, 100, 0.3);
222
+ }
223
+
224
+ .status.error {
225
+ display: block;
226
+ background: rgba(255, 59, 48, 0.15);
227
+ color: #ff3b30;
228
+ border: 1px solid rgba(255, 59, 48, 0.3);
229
+ }
230
+
231
+ .loader {
232
+ width: 22px;
233
+ height: 22px;
234
+ border: 3px solid rgba(255, 193, 7, 0.3);
235
+ border-radius: 50%;
236
+ border-top-color: #ffc107;
237
+ animation: spin 1s linear infinite;
238
+ }
239
+
240
+ @keyframes spin {
241
+ to { transform: rotate(360deg); }
242
+ }
243
+
244
+ .features {
245
+ margin-top: 35px;
246
+ display: grid;
247
+ grid-template-columns: repeat(3, 1fr);
248
+ gap: 15px;
249
+ }
250
+
251
+ .feature {
252
+ text-align: center;
253
+ padding: 20px 15px;
254
+ background: rgba(255, 255, 255, 0.03);
255
+ border-radius: 16px;
256
+ border: 1px solid rgba(255, 255, 255, 0.05);
257
+ transition: all 0.3s ease;
258
+ }
259
+
260
+ .feature:hover {
261
+ background: rgba(255, 255, 255, 0.08);
262
+ transform: translateY(-3px);
263
+ }
264
+
265
+ .feature-icon {
266
+ font-size: 2rem;
267
+ margin-bottom: 10px;
268
+ }
269
+
270
+ .feature-title {
271
+ color: #fff;
272
+ font-weight: 600;
273
+ font-size: 0.9rem;
274
+ margin-bottom: 5px;
275
+ }
276
+
277
+ .feature-desc {
278
+ color: rgba(255, 255, 255, 0.5);
279
+ font-size: 0.75rem;
280
+ }
281
+
282
+ .info-bar {
283
+ margin-top: 30px;
284
+ padding: 15px 20px;
285
+ background: rgba(102, 126, 234, 0.1);
286
+ border-radius: 12px;
287
+ border: 1px solid rgba(102, 126, 234, 0.2);
288
+ }
289
+
290
+ .info-bar p {
291
+ color: rgba(255, 255, 255, 0.7);
292
+ font-size: 0.85rem;
293
+ text-align: center;
294
+ }
295
+
296
+ .info-bar a {
297
+ color: #667eea;
298
+ text-decoration: none;
299
+ }
300
+
301
+ .supported-sites {
302
+ margin-top: 25px;
303
+ text-align: center;
304
+ }
305
+
306
+ .supported-sites p {
307
+ color: rgba(255, 255, 255, 0.5);
308
+ font-size: 0.8rem;
309
+ margin-bottom: 10px;
310
+ }
311
+
312
+ .site-badges {
313
+ display: flex;
314
+ flex-wrap: wrap;
315
+ justify-content: center;
316
+ gap: 8px;
317
+ }
318
+
319
+ .badge {
320
+ padding: 6px 12px;
321
+ background: rgba(255, 255, 255, 0.08);
322
+ border-radius: 20px;
323
+ color: rgba(255, 255, 255, 0.7);
324
+ font-size: 0.75rem;
325
+ }
326
+
327
+ @media (max-width: 600px) {
328
+ .container {
329
+ padding: 25px;
330
+ }
331
+
332
+ h1 {
333
+ font-size: 1.6rem;
334
+ }
335
+
336
+ .btn-group {
337
+ grid-template-columns: 1fr;
338
+ }
339
+
340
+ .features {
341
+ grid-template-columns: 1fr;
342
+ }
343
+
344
+ .logo {
345
+ font-size: 2.5rem;
346
+ }
347
+ }
348
+ </style>
349
+ </head>
350
+ <body>
351
+ <div class="container">
352
+ <div class="header">
353
+ <div class="logo">🎬</div>
354
+ <h1>Media Downloader</h1>
355
+ <p class="subtitle">Download videos as MP4 or extract audio as MP3</p>
356
+ </div>
357
+
358
+ <div class="input-section">
359
+ <div class="input-wrapper">
360
+ <span class="input-icon">🔗</span>
361
+ <input type="text" id="url" placeholder="Paste video URL here..." autocomplete="off">
362
+ </div>
363
+ </div>
364
+
365
+ <div class="btn-group">
366
+ <button class="btn btn-mp4" onclick="download('mp4')" id="btn-mp4">
367
+ <span class="btn-icon">📹</span>
368
+ <span>Download MP4</span>
369
+ </button>
370
+ <button class="btn btn-mp3" onclick="download('mp3')" id="btn-mp3">
371
+ <span class="btn-icon">🎵</span>
372
+ <span>Download MP3</span>
373
+ </button>
374
+ </div>
375
+
376
+ <div id="status" class="status"></div>
377
+
378
+ <div class="features">
379
+ <div class="feature">
380
+ <div class="feature-icon">⚡</div>
381
+ <div class="feature-title">Fast</div>
382
+ <div class="feature-desc">Quick processing</div>
383
+ </div>
384
+ <div class="feature">
385
+ <div class="feature-icon">🎯</div>
386
+ <div class="feature-title">Quality</div>
387
+ <div class="feature-desc">Best available</div>
388
+ </div>
389
+ <div class="feature">
390
+ <div class="feature-icon">🔒</div>
391
+ <div class="feature-title">Secure</div>
392
+ <div class="feature-desc">No data stored</div>
393
+ </div>
394
+ </div>
395
+
396
+ <div class="supported-sites">
397
+ <p>Supported platforms:</p>
398
+ <div class="site-badges">
399
+ <span class="badge">YouTube</span>
400
+ <span class="badge">Twitter/X</span>
401
+ <span class="badge">Facebook</span>
402
+ <span class="badge">Instagram</span>
403
+ <span class="badge">TikTok</span>
404
+ <span class="badge">Vimeo</span>
405
+ <span class="badge">+1000 more</span>
406
+ </div>
407
+ </div>
408
+
409
+ <div class="info-bar">
410
+ <p>💡 Tip: Files are automatically deleted after download for your privacy</p>
411
+ </div>
412
+ </div>
413
+
414
+ <script>
415
+ async function download(format) {
416
+ const url = document.getElementById('url').value.trim();
417
+ const status = document.getElementById('status');
418
+ const btnMp4 = document.getElementById('btn-mp4');
419
+ const btnMp3 = document.getElementById('btn-mp3');
420
+
421
+ if (!url) {
422
+ showStatus('error', '❌ Please enter a valid URL');
423
+ shakeInput();
424
+ return;
425
+ }
426
+
427
+ // Basic URL validation
428
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
429
+ showStatus('error', '❌ Please enter a valid URL starting with http:// or https://');
430
+ shakeInput();
431
+ return;
432
+ }
433
+
434
+ // Disable buttons
435
+ btnMp4.disabled = true;
436
+ btnMp3.disabled = true;
437
+
438
+ // Show loading
439
+ showStatus('loading', 'Processing your request...');
440
+
441
+ try {
442
+ const response = await fetch('/download', {
443
+ method: 'POST',
444
+ headers: {
445
+ 'Content-Type': 'application/json',
446
+ },
447
+ body: JSON.stringify({ url: url, format: format })
448
+ });
449
+
450
+ if (!response.ok) {
451
+ const error = await response.json();
452
+ throw new Error(error.error || 'Download failed');
453
+ }
454
+
455
+ // Get filename from header
456
+ const contentDisposition = response.headers.get('Content-Disposition');
457
+ let filename = `download.${format}`;
458
+ if (contentDisposition) {
459
+ const match = contentDisposition.match(/filename[^;=\\n]*=((['"]).*?\\2|[^;\\n]*)/);
460
+ if (match && match[1]) {
461
+ filename = match[1].replace(/['"]/g, '');
462
+ }
463
+ }
464
+
465
+ // Download file
466
+ const blob = await response.blob();
467
+ const downloadUrl = window.URL.createObjectURL(blob);
468
+ const a = document.createElement('a');
469
+ a.href = downloadUrl;
470
+ a.download = filename;
471
+ document.body.appendChild(a);
472
+ a.click();
473
+ window.URL.revokeObjectURL(downloadUrl);
474
+ a.remove();
475
+
476
+ showStatus('success', '✅ Download started successfully!');
477
+ document.getElementById('url').value = '';
478
+
479
+ } catch (error) {
480
+ showStatus('error', '❌ ' + error.message);
481
+ } finally {
482
+ btnMp4.disabled = false;
483
+ btnMp3.disabled = false;
484
+ }
485
+ }
486
+
487
+ function showStatus(type, message) {
488
+ const status = document.getElementById('status');
489
+ status.className = 'status ' + type;
490
+ if (type === 'loading') {
491
+ status.innerHTML = '<div class="loader"></div><span>' + message + '</span>';
492
+ } else {
493
+ status.textContent = message;
494
+ }
495
+ }
496
+
497
+ function shakeInput() {
498
+ const input = document.getElementById('url');
499
+ input.style.animation = 'shake 0.5s ease';
500
+ setTimeout(() => {
501
+ input.style.animation = '';
502
+ }, 500);
503
+ }
504
+
505
+ // Add shake animation
506
+ const style = document.createElement('style');
507
+ style.textContent = `
508
+ @keyframes shake {
509
+ 0%, 100% { transform: translateX(0); }
510
+ 25% { transform: translateX(-10px); }
511
+ 75% { transform: translateX(10px); }
512
+ }
513
+ `;
514
+ document.head.appendChild(style);
515
+
516
+ // Allow Enter key to trigger MP4 download
517
+ document.getElementById('url').addEventListener('keypress', function(e) {
518
+ if (e.key === 'Enter') {
519
+ download('mp4');
520
+ }
521
+ });
522
+
523
+ // Focus input on load
524
+ window.onload = function() {
525
+ document.getElementById('url').focus();
526
+ };
527
+ </script>
528
+ </body>
529
+ </html>
530
+ """
531
+
532
+
533
+ def clean_filename(title):
534
+ """Clean filename to remove invalid characters"""
535
+ clean = re.sub(r'[<>:"/\\|?*]', '', title)
536
+ clean = re.sub(r'\s+', ' ', clean).strip()
537
+ return clean[:100] if len(clean) > 100 else clean
538
+
539
+
540
+ def cleanup_old_files():
541
+ """Clean up files older than 5 minutes"""
542
+ try:
543
+ now = time.time()
544
+ for filename in os.listdir(DOWNLOAD_DIR):
545
+ filepath = os.path.join(DOWNLOAD_DIR, filename)
546
+ if os.path.isfile(filepath):
547
+ if now - os.path.getmtime(filepath) > 300: # 5 minutes
548
+ os.remove(filepath)
549
+ except Exception as e:
550
+ print(f"Cleanup error: {e}")
551
+
552
+
553
+ @app.route('/')
554
+ def index():
555
+ # Cleanup old files on each visit
556
+ threading.Thread(target=cleanup_old_files, daemon=True).start()
557
+ return render_template_string(HTML_TEMPLATE)
558
+
559
+
560
+ @app.route('/download', methods=['POST'])
561
+ def download_video():
562
+ try:
563
+ data = request.get_json()
564
+ url = data.get('url', '').strip()
565
+ format_type = data.get('format', 'mp4')
566
+
567
+ if not url:
568
+ return jsonify({'error': 'URL is required'}), 400
569
+
570
+ # Validate URL format
571
+ if not url.startswith(('http://', 'https://')):
572
+ return jsonify({'error': 'Invalid URL format'}), 400
573
+
574
+ # Generate unique filename
575
+ unique_id = str(uuid.uuid4())[:8]
576
+
577
+ # Configure yt-dlp options
578
+ if format_type == 'mp3':
579
+ ydl_opts = {
580
+ 'format': 'bestaudio/best',
581
+ 'postprocessors': [{
582
+ 'key': 'FFmpegExtractAudio',
583
+ 'preferredcodec': 'mp3',
584
+ 'preferredquality': '192',
585
+ }],
586
+ 'outtmpl': os.path.join(DOWNLOAD_DIR, f'{unique_id}.%(ext)s'),
587
+ 'quiet': True,
588
+ 'no_warnings': True,
589
+ 'extract_flat': False,
590
+ 'noplaylist': True,
591
+ }
592
+ expected_ext = 'mp3'
593
+ else:
594
+ ydl_opts = {
595
+ 'format': 'best[ext=mp4]/bestvideo[ext=mp4]+bestaudio[ext=m4a]/best',
596
+ 'outtmpl': os.path.join(DOWNLOAD_DIR, f'{unique_id}.%(ext)s'),
597
+ 'quiet': True,
598
+ 'no_warnings': True,
599
+ 'noplaylist': True,
600
+ 'merge_output_format': 'mp4',
601
+ }
602
+ expected_ext = 'mp4'
603
+
604
+ # Download the video/audio
605
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
606
+ info = ydl.extract_info(url, download=True)
607
+ title = info.get('title', 'download')
608
+ clean_title = clean_filename(title)
609
+
610
+ # Find the downloaded file
611
+ file_path = None
612
+ possible_extensions = ['mp3'] if format_type == 'mp3' else ['mp4', 'webm', 'mkv', 'avi', 'mov']
613
+
614
+ for ext in possible_extensions:
615
+ potential_path = os.path.join(DOWNLOAD_DIR, f'{unique_id}.{ext}')
616
+ if os.path.exists(potential_path):
617
+ file_path = potential_path
618
+ expected_ext = ext
619
+ break
620
+
621
+ if not file_path or not os.path.exists(file_path):
622
+ return jsonify({'error': 'Failed to download file'}), 500
623
+
624
+ download_name = f'{clean_title}.{expected_ext}'
625
+
626
+ # Send file
627
+ response = send_file(
628
+ file_path,
629
+ as_attachment=True,
630
+ download_name=download_name,
631
+ mimetype='audio/mpeg' if format_type == 'mp3' else 'video/mp4'
632
+ )
633
+
634
+ # Schedule file deletion after sending
635
+ def delete_file_later():
636
+ time.sleep(30)
637
+ try:
638
+ if os.path.exists(file_path):
639
+ os.remove(file_path)
640
+ except Exception as e:
641
+ print(f"Error deleting file: {e}")
642
+
643
+ threading.Thread(target=delete_file_later, daemon=True).start()
644
+
645
+ return response
646
+
647
+ except yt_dlp.utils.DownloadError as e:
648
+ error_msg = str(e)
649
+ if 'Private video' in error_msg:
650
+ return jsonify({'error': 'This video is private'}), 400
651
+ elif 'Video unavailable' in error_msg:
652
+ return jsonify({'error': 'Video is unavailable'}), 400
653
+ elif 'age' in error_msg.lower():
654
+ return jsonify({'error': 'Age-restricted content cannot be downloaded'}), 400
655
+ else:
656
+ return jsonify({'error': 'Failed to download. Please check the URL'}), 400
657
+ except Exception as e:
658
+ print(f"Error: {e}")
659
+ return jsonify({'error': 'An error occurred. Please try again'}), 500
660
+
661
+
662
+ @app.route('/health')
663
+ def health():
664
+ return jsonify({'status': 'healthy'})
665
+
666
+
667
+ if __name__ == '__main__':
668
+ app.run(host='0.0.0.0', port=7860, debug=False)