Download / app.py
factblink514's picture
Update app.py
2590ba4 verified
Raw
History Blame Contribute Delete
15.9 kB
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'
})
@app.route('/')
def home():
return render_template_string(HTML_TEMPLATE)
@app.route('/api/analyze', methods=['POST'])
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]}'})
@app.route('/api/download', methods=['POST'])
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})
@app.route('/api/status/<uid>')
def api_status(uid):
return jsonify(progress_db.get(uid, {'status': 'none'}))
@app.route('/api/file/<name>')
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)