Spaces:
Runtime error
Runtime error
| import os | |
| import uuid | |
| import tempfile | |
| import threading | |
| from flask import Flask, render_template_string, request, jsonify, send_file | |
| import yt_dlp | |
| app = Flask(__name__) | |
| TEMP_DIR = os.path.join(tempfile.gettempdir(), 'termux_dl_final') | |
| if not os.path.exists(TEMP_DIR): | |
| os.makedirs(TEMP_DIR) | |
| progress_db = {} | |
| HTML_TEMPLATE = """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Deepu Downloader</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap'); | |
| * { font-family: 'Inter', sans-serif; } | |
| body { background: #0a0a0f; color: white; min-height: 100vh; } | |
| .glass { background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); backdrop-filter: blur(10px); border-radius: 1rem; } | |
| .btn-analyze { background: linear-gradient(135deg, #6366f1, #8b5cf6); transition: all 0.2s; } | |
| .btn-analyze:hover { opacity: 0.85; transform: scale(1.02); } | |
| .btn-analyze:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } | |
| .fmt-btn { border: 1px solid rgba(255,255,255,0.15); background: rgba(255,255,255,0.05); transition: all 0.2s; border-radius: 0.5rem; padding: 0.5rem; font-size: 0.75rem; color: #ccc; cursor: pointer; } | |
| .fmt-btn:hover { border-color: #6366f1; color: white; } | |
| .fmt-btn.active { background: #6366f1; border-color: #6366f1; color: white; font-weight: 700; } | |
| .progress-bar { transition: width 0.5s ease; background: linear-gradient(90deg, #6366f1, #a855f7); } | |
| .glow { box-shadow: 0 0 30px rgba(99,102,241,0.2); } | |
| input { background: transparent; border: none; outline: none; color: white; width: 100%; } | |
| input::placeholder { color: #555; } | |
| </style> | |
| </head> | |
| <body class="p-4"> | |
| <div class="max-w-md mx-auto mt-8"> | |
| <!-- Header --> | |
| <div class="text-center mb-8"> | |
| <h1 class="text-3xl font-bold mb-1" style="background: linear-gradient(135deg, #6366f1, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent;"> | |
| Deepu Downloader | |
| </h1> | |
| <p class="text-gray-500 text-sm">YouTube Video & MP3 Downloader</p> | |
| </div> | |
| <!-- URL Input --> | |
| <div class="glass p-4 flex gap-3 mb-5 glow"> | |
| <i class="fas fa-link text-indigo-400 mt-1"></i> | |
| <input id="urlInput" type="text" placeholder="YouTube link paste karo..."> | |
| <button onclick="analyze()" id="analyzeBtn" class="btn-analyze px-5 py-2 rounded-lg font-bold text-sm whitespace-nowrap"> | |
| Analyze | |
| </button> | |
| </div> | |
| <!-- Error Box --> | |
| <div id="errBox" class="hidden glass p-4 mb-5 border border-red-500/30"> | |
| <p class="text-red-400 text-sm"><i class="fas fa-exclamation-circle mr-2"></i><span id="errMsg"></span></p> | |
| </div> | |
| <!-- Video Info --> | |
| <div id="infoBox" class="hidden glass p-4 mb-5"> | |
| <img id="thumb" class="w-full rounded-lg mb-4 object-cover" style="max-height: 200px;" src=""> | |
| <h2 id="title" class="font-semibold mb-4 text-sm leading-relaxed text-gray-200 line-clamp-2"></h2> | |
| <p class="text-xs text-gray-500 mb-2 uppercase tracking-wider">Format chuno</p> | |
| <div id="formats" class="grid grid-cols-3 gap-2 mb-5"></div> | |
| <button onclick="startDownload()" class="w-full py-3 rounded-lg font-bold text-sm" style="background: linear-gradient(135deg, #6366f1, #a855f7);"> | |
| <i class="fas fa-download mr-2"></i>Download Karo | |
| </button> | |
| </div> | |
| <!-- Progress --> | |
| <div id="progBox" class="hidden glass p-5 mb-5"> | |
| <div class="flex justify-between items-center mb-3"> | |
| <span id="statusText" class="text-sm text-indigo-400 font-semibold">Downloading...</span> | |
| <span id="percent" class="text-sm font-bold">0%</span> | |
| </div> | |
| <div class="w-full h-2 bg-gray-800 rounded-full overflow-hidden mb-3"> | |
| <div id="bar" class="progress-bar h-full rounded-full" style="width: 0%"></div> | |
| </div> | |
| <div class="flex justify-between text-xs text-gray-500"> | |
| <span id="speedTxt">Speed: --</span> | |
| <span id="etaTxt">ETA: --</span> | |
| </div> | |
| </div> | |
| <!-- Done --> | |
| <div id="doneBox" class="hidden glass p-5 text-center" style="border-color: rgba(34,197,94,0.3);"> | |
| <div class="text-4xl mb-3">✅</div> | |
| <p class="font-bold text-green-400 mb-1">Download Ready!</p> | |
| <p class="text-xs text-gray-500 mb-4">File save karne ke liye button dabao</p> | |
| <a id="dlLink" href="#" class="inline-block px-8 py-3 rounded-lg font-bold text-sm" style="background: linear-gradient(135deg, #16a34a, #22c55e);"> | |
| <i class="fas fa-save mr-2"></i>Save File | |
| </a> | |
| <br><br> | |
| <button onclick="reset()" class="text-xs text-gray-500 hover:text-white transition-colors"> | |
| <i class="fas fa-redo mr-1"></i>Naya Download | |
| </button> | |
| </div> | |
| <p class="text-center text-xs text-gray-700 mt-6">Made by Deepu ❤️</p> | |
| </div> | |
| <script> | |
| let videoData = null; | |
| let selectedFmt = null; | |
| let currentId = null; | |
| let pollTimer = null; | |
| function showErr(msg) { | |
| document.getElementById('errMsg').innerText = msg; | |
| document.getElementById('errBox').classList.remove('hidden'); | |
| } | |
| function hideErr() { | |
| document.getElementById('errBox').classList.add('hidden'); | |
| } | |
| function reset() { | |
| videoData = null; | |
| selectedFmt = null; | |
| currentId = null; | |
| if(pollTimer) clearInterval(pollTimer); | |
| document.getElementById('urlInput').value = ''; | |
| document.getElementById('infoBox').classList.add('hidden'); | |
| document.getElementById('progBox').classList.add('hidden'); | |
| document.getElementById('doneBox').classList.add('hidden'); | |
| hideErr(); | |
| } | |
| function analyze() { | |
| const url = document.getElementById('urlInput').value.trim(); | |
| if(!url) { showErr('Pehle URL daalo bhai!'); return; } | |
| hideErr(); | |
| const btn = document.getElementById('analyzeBtn'); | |
| btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i>...'; | |
| btn.disabled = true; | |
| document.getElementById('infoBox').classList.add('hidden'); | |
| document.getElementById('doneBox').classList.add('hidden'); | |
| document.getElementById('progBox').classList.add('hidden'); | |
| fetch('/api/analyze', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({url}) | |
| }) | |
| .then(r => r.json()) | |
| .then(data => { | |
| if(data.success) { | |
| videoData = data; | |
| selectedFmt = null; | |
| document.getElementById('infoBox').classList.remove('hidden'); | |
| document.getElementById('thumb').src = data.thumbnail || ''; | |
| document.getElementById('title').innerText = data.title || 'Unknown Title'; | |
| const fDiv = document.getElementById('formats'); | |
| fDiv.innerHTML = ''; | |
| // MP3 button | |
| const mp3 = document.createElement('button'); | |
| mp3.className = 'fmt-btn'; | |
| mp3.innerHTML = '<i class="fas fa-music mr-1"></i>MP3'; | |
| mp3.onclick = () => setFmt('mp3', mp3); | |
| fDiv.appendChild(mp3); | |
| // Video formats | |
| data.formats.forEach(f => { | |
| const b = document.createElement('button'); | |
| b.className = 'fmt-btn'; | |
| b.innerHTML = `<i class="fas fa-video mr-1"></i>${f.h}p`; | |
| b.onclick = () => setFmt(f.id, b); | |
| fDiv.appendChild(b); | |
| }); | |
| } else { | |
| showErr(data.message || 'Kuch gadbad ho gayi!'); | |
| } | |
| }) | |
| .catch(e => showErr('Network error: ' + e.message)) | |
| .finally(() => { | |
| btn.innerHTML = 'Analyze'; | |
| btn.disabled = false; | |
| }); | |
| } | |
| function setFmt(id, btn) { | |
| selectedFmt = id; | |
| document.querySelectorAll('.fmt-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| } | |
| function startDownload() { | |
| if(!selectedFmt) { showErr('Pehle format chuno!'); return; } | |
| hideErr(); | |
| document.getElementById('infoBox').classList.add('hidden'); | |
| document.getElementById('progBox').classList.remove('hidden'); | |
| document.getElementById('bar').style.width = '0%'; | |
| document.getElementById('percent').innerText = '0%'; | |
| document.getElementById('statusText').innerText = 'Starting...'; | |
| document.getElementById('speedTxt').innerText = 'Speed: --'; | |
| document.getElementById('etaTxt').innerText = 'ETA: --'; | |
| fetch('/api/download', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({url: videoData.url, format: selectedFmt}) | |
| }) | |
| .then(r => r.json()) | |
| .then(data => { | |
| currentId = data.id; | |
| pollTimer = setInterval(pollStatus, 1200); | |
| }) | |
| .catch(e => { | |
| document.getElementById('progBox').classList.add('hidden'); | |
| showErr('Download start nahi hua: ' + e.message); | |
| }); | |
| } | |
| function pollStatus() { | |
| fetch('/api/status/' + currentId) | |
| .then(r => r.json()) | |
| .then(s => { | |
| if(s.status === 'done') { | |
| clearInterval(pollTimer); | |
| document.getElementById('progBox').classList.add('hidden'); | |
| document.getElementById('doneBox').classList.remove('hidden'); | |
| const fname = videoData.title ? videoData.title.replace(/[^a-zA-Z0-9]/g, '_') : 'video'; | |
| document.getElementById('dlLink').href = '/api/file/' + s.file; | |
| document.getElementById('dlLink').download = fname; | |
| } else if(s.status === 'error') { | |
| clearInterval(pollTimer); | |
| document.getElementById('progBox').classList.add('hidden'); | |
| showErr('Download fail ho gaya. Dobara try karo.'); | |
| } else { | |
| const p = parseFloat(s.p) || 0; | |
| document.getElementById('bar').style.width = p + '%'; | |
| document.getElementById('percent').innerText = Math.round(p) + '%'; | |
| document.getElementById('statusText').innerText = 'Downloading...'; | |
| document.getElementById('speedTxt').innerText = 'Speed: ' + (s.s || '--'); | |
| document.getElementById('etaTxt').innerText = 'ETA: ' + (s.e || '--'); | |
| } | |
| }) | |
| .catch(() => {}); // silently ignore poll errors | |
| } | |
| // Enter key support | |
| document.addEventListener('DOMContentLoaded', () => { | |
| document.getElementById('urlInput').addEventListener('keypress', e => { | |
| if(e.key === 'Enter') analyze(); | |
| }); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| def hook(d, uid): | |
| if uid not in progress_db: | |
| return | |
| if d['status'] == 'downloading': | |
| raw = d.get('_percent_str', '0%').replace('%', '').replace('\x1b[0;94m', '').replace('\x1b[0m', '').strip() | |
| try: | |
| p = float(raw) | |
| except: | |
| p = 0 | |
| progress_db[uid].update({ | |
| 'p': p, | |
| 's': d.get('_speed_str', 'N/A'), | |
| 'e': d.get('_eta_str', 'N/A'), | |
| 'status': 'dl' | |
| }) | |
| def home(): | |
| return render_template_string(HTML_TEMPLATE) | |
| def api_analyze(): | |
| url = request.json.get('url', '').strip() | |
| if not url: | |
| return jsonify({'success': False, 'message': 'URL daalo bhai!'}) | |
| opts = { | |
| 'quiet': True, | |
| 'noplaylist': True, | |
| 'skip_download': True, | |
| } | |
| try: | |
| with yt_dlp.YoutubeDL(opts) as ydl: | |
| info = ydl.extract_info(url, download=False) | |
| fmts = [] | |
| seen = set() | |
| for f in info.get('formats', []): | |
| h = f.get('height') | |
| if h and f.get('vcodec', 'none') != 'none' and h not in seen: | |
| seen.add(h) | |
| fmts.append({'id': f['format_id'], 'h': h}) | |
| # Sort by height descending, take top 8 | |
| fmts = sorted(fmts, key=lambda x: x['h'], reverse=True)[:8] | |
| return jsonify({ | |
| 'success': True, | |
| 'title': info.get('title', 'Unknown'), | |
| 'thumbnail': info.get('thumbnail', ''), | |
| 'formats': fmts, | |
| 'url': url | |
| }) | |
| except yt_dlp.utils.DownloadError as e: | |
| return jsonify({'success': False, 'message': str(e)[:200]}) | |
| except Exception as e: | |
| return jsonify({'success': False, 'message': f'Error: {str(e)[:200]}'}) | |
| def api_download(): | |
| data = request.json | |
| uid = str(uuid.uuid4()) | |
| progress_db[uid] = {'p': 0, 's': '', 'e': '', 'status': 'start'} | |
| def dl(): | |
| path = os.path.join(TEMP_DIR, f"{uid}.%(ext)s") | |
| opts = { | |
| 'outtmpl': path, | |
| 'progress_hooks': [lambda d: hook(d, uid)], | |
| 'quiet': True, | |
| 'noplaylist': True, | |
| } | |
| if data.get('format') == 'mp3': | |
| opts.update({ | |
| 'format': 'bestaudio/best', | |
| 'postprocessors': [{'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3', 'preferredquality': '192'}] | |
| }) | |
| else: | |
| fmt_id = data.get('format', 'best') | |
| opts.update({ | |
| 'format': f"{fmt_id}+bestaudio/best", | |
| 'merge_output_format': 'mp4' | |
| }) | |
| try: | |
| with yt_dlp.YoutubeDL(opts) as ydl: | |
| info = ydl.extract_info(data['url'], download=True) | |
| fname = ydl.prepare_filename(info) | |
| if data.get('format') == 'mp3': | |
| fname = os.path.splitext(fname)[0] + '.mp3' | |
| elif not fname.endswith('.mp4'): | |
| fname = os.path.splitext(fname)[0] + '.mp4' | |
| progress_db[uid].update({'status': 'done', 'file': os.path.basename(fname)}) | |
| except Exception as e: | |
| progress_db[uid]['status'] = 'error' | |
| print(f"Download error: {e}") | |
| threading.Thread(target=dl, daemon=True).start() | |
| return jsonify({'id': uid}) | |
| def api_status(uid): | |
| return jsonify(progress_db.get(uid, {'status': 'none'})) | |
| def api_file(name): | |
| fpath = os.path.join(TEMP_DIR, name) | |
| if not os.path.exists(fpath): | |
| return jsonify({'error': 'File nahi mili'}), 404 | |
| return send_file(fpath, as_attachment=True) | |
| if __name__ == '__main__': | |
| app.run(host='0.0.0.0', port=5000, threaded=True) | |