Files
youtube-downloader/app.py
renato97 c20b40b57c 🎉 Inicializar YouTube Downloader Dashboard
-  Dashboard web moderno con Flask y Bootstrap 5
-  Descarga de videos en formato MP3 y MP4
-  Configuración optimizada de yt-dlp para evitar errores 403
-  Progreso en tiempo real con velocidad y ETA
-  Soporte Docker con docker-compose
-  Script de despliegue automático
-  API REST para integraciones
-  Manejo robusto de errores con reintentos
-  Limpieza automática de archivos temporales
-  README detallado con instrucciones de uso

🚀 Funciona con YouTube y está listo para producción 24/7

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 15:03:45 +00:00

227 lines
8.1 KiB
Python

from flask import Flask, render_template, request, jsonify, send_file, redirect, url_for
import yt_dlp
import os
import uuid
from datetime import datetime
import threading
import time
app = Flask(__name__)
app.config['DOWNLOAD_FOLDER'] = 'static/downloads'
if not os.path.exists(app.config['DOWNLOAD_FOLDER']):
os.makedirs(app.config['DOWNLOAD_FOLDER'])
download_status = {}
class DownloadProgress:
def __init__(self, download_id):
self.download_id = download_id
self.status = 'starting'
self.progress = 0
self.speed = ''
self.eta = ''
self.filename = ''
self.error = None
self.complete = False
def update(self, d):
if d['status'] == 'downloading':
self.status = 'downloading'
self.progress = d.get('_percent_str', '0%').strip('%')
self.speed = d.get('_speed_str', '')
self.eta = d.get('_eta_str', '')
self.filename = d.get('filename', '')
elif d['status'] == 'finished':
self.status = 'finished'
self.complete = True
self.progress = 100
elif d['status'] == 'error':
self.status = 'error'
self.error = d.get('error', 'Unknown error')
def hook(d, download_id):
if download_id in download_status:
download_status[download_id].update(d)
def download_video(url, download_id, format_type='mp4'):
progress = DownloadProgress(download_id)
download_status[download_id] = progress
try:
ydl_opts = {
'outtmpl': os.path.join(app.config['DOWNLOAD_FOLDER'], f'{download_id}.%(ext)s'),
'progress_hooks': [lambda d: hook(d, download_id)],
'quiet': True,
'no_warnings': True,
# Configuración funcional probada con yt-dlp 2025.10.22
'extract_flat': False,
'restrictfilenames': True,
'no_check_certificate': True,
'socket_timeout': 60,
'retries': 10,
'fragment_retries': 15,
'extractor_retries': 10,
'file_access_retries': 10,
'ignoreerrors': False,
'user_agent': 'yt-dlp/2025.10.22',
'http_headers': {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'DNT': '1',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-User': '?1',
'sec-ch-ua': '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"'
}
}
if format_type == 'mp3':
ydl_opts.update({
'format': 'bestaudio[ext=m4a]/bestaudio/best',
'postprocessors': [{
'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3',
'preferredquality': '192',
}],
'writethumbnail': False,
'embed_thumbnail': False,
'addmetadata': True,
})
else: # mp4
ydl_opts.update({
'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
'merge_output_format': 'mp4',
})
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.extract_info(url, download=True)
except yt_dlp.utils.DownloadError as e:
progress.status = 'error'
if "403" in str(e):
progress.error = "Error de acceso (403). El video puede estar restringido o no disponible. Intenta con otro video."
else:
progress.error = f"Error de descarga: {str(e)}"
except Exception as e:
progress.status = 'error'
if "403" in str(e):
progress.error = "Error de acceso (403). YouTube ha bloqueado la solicitud. Intenta nuevamente en unos minutos."
else:
progress.error = f"Error inesperado: {str(e)}"
@app.route('/')
def index():
return render_template('index.html')
@app.route('/api/downloads', methods=['POST'])
def start_download():
data = request.get_json()
url = data.get('url')
format_type = data.get('format', 'mp4')
if not url:
return jsonify({'error': 'URL is required'}), 400
download_id = str(uuid.uuid4())
# Start download in background thread
thread = threading.Thread(target=download_video, args=(url, download_id, format_type))
thread.daemon = True
thread.start()
return jsonify({'download_id': download_id})
@app.route('/api/status/<download_id>')
def get_status(download_id):
if download_id is None or download_id == 'null' or download_id not in download_status:
return jsonify({'error': 'Download not found'}), 404
progress = download_status[download_id]
response = {
'status': progress.status,
'progress': progress.progress,
'speed': progress.speed,
'eta': progress.eta,
'filename': progress.filename,
'error': progress.error,
'complete': progress.complete
}
return jsonify(response)
@app.route('/api/downloads')
def list_downloads():
downloads = []
try:
for filename in os.listdir(app.config['DOWNLOAD_FOLDER']):
if filename.endswith(('.mp4', '.mp3', '.webm', '.m4a')):
file_path = os.path.join(app.config['DOWNLOAD_FOLDER'], filename)
file_size = os.path.getsize(file_path)
modified_time = os.path.getmtime(file_path)
modified_time = datetime.fromtimestamp(modified_time).strftime('%Y-%m-%d %H:%M:%S')
downloads.append({
'filename': filename,
'size': file_size,
'modified': modified_time,
'url': url_for('download_file', filename=filename)
})
except Exception as e:
print(f"Error listing downloads: {e}")
return jsonify(downloads)
@app.route('/api/cleanup', methods=['POST'])
def cleanup_downloads():
"""Limpia descargas fallidas y muy antiguas"""
try:
cleaned_files = []
current_time = time.time()
# Limpiar archivos temporales (menores de 1KB y más antiguos de 1 hora)
for filename in os.listdir(app.config['DOWNLOAD_FOLDER']):
file_path = os.path.join(app.config['DOWNLOAD_FOLDER'], filename)
if os.path.isfile(file_path):
file_size = os.path.getsize(file_path)
file_age = current_time - os.path.getmtime(file_path)
# Eliminar archivos muy pequeños o muy antiguos
if (file_size < 1024 and file_age > 3600) or file_age > 86400 * 7: # 7 días
os.remove(file_path)
cleaned_files.append(filename)
# Limpiar estados de descarga antiguos
to_remove = []
for download_id, progress in download_status.items():
if progress.complete or progress.status == 'error':
to_remove.append(download_id)
for download_id in to_remove:
del download_status[download_id]
return jsonify({
'cleaned_files': cleaned_files,
'cleaned_downloads': len(to_remove)
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/download/<filename>')
def download_file(filename):
file_path = os.path.join(app.config['DOWNLOAD_FOLDER'], filename)
if os.path.exists(file_path):
return send_file(file_path, as_attachment=True)
return "File not found", 404
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)