Files
youtube-downloader/app.py
renato97 a5332b2d38 Añadir detección automática de FFmpeg
- Agregar funciones check_ffmpeg() y install_ffmpeg() para detectar e instalar FFmpeg
- Implementar verificación previa a descargas MP3 para asegurar disponibilidad de FFmpeg
- Crear endpoints /api/ffmpeg/status y /api/ffmpeg/install para gestión de FFmpeg
- Mejorar frontend con detección de estado de FFmpeg y opción de instalación automática
- Deshabilitar opción MP3 si FFmpeg no está disponible
- Añadir mensajes de error específicos para problemas de FFmpeg

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

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

313 lines
11 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 subprocess
import shutil
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 = {}
def check_ffmpeg():
"""Verificar si FFmpeg está disponible en el sistema"""
try:
result = subprocess.run(['ffmpeg', '-version'],
capture_output=True, text=True, timeout=10)
return result.returncode == 0
except (subprocess.TimeoutExpired, FileNotFoundError):
return False
def install_ffmpeg():
"""Intentar instalar FFmpeg automáticamente"""
try:
# Detectar el sistema operativo
import platform
system = platform.system().lower()
if system == 'linux':
# Para sistemas basados en Debian/Ubuntu
commands = [
['sudo', 'apt-get', 'update'],
['sudo', 'apt-get', 'install', '-y', 'ffmpeg']
]
elif system == 'darwin':
# Para macOS usando Homebrew
commands = [
['brew', 'update'],
['brew', 'install', 'ffmpeg']
]
elif system == 'windows':
# Para Windows (simplificado - requeriría manual)
return False
else:
return False
for cmd in commands:
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
return False
return True
except Exception:
return False
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
# Verificar FFmpeg para formatos que lo requieren
if format_type == 'mp3':
if not check_ffmpeg():
progress.status = 'error'
progress.error = "FFmpeg no está disponible. La conversión a MP3 requiere FFmpeg. Por favor, instala FFmpeg o contacta al administrador."
return
# Opcional: intentar instalar FFmpeg automáticamente si no está disponible
# if install_ffmpeg():
# progress.status = 'info'
# progress.error = "FFmpeg ha sido instalado automáticamente. Intenta la descarga nuevamente."
# else:
# progress.status = 'error'
# progress.error = "No se pudo instalar FFmpeg automáticamente. La conversión a MP3 requiere FFmpeg."
# return
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/ffmpeg/status')
def ffmpeg_status():
"""Verificar el estado de FFmpeg"""
ffmpeg_available = check_ffmpeg()
return jsonify({
'available': ffmpeg_available,
'can_install': True if not ffmpeg_available else False
})
@app.route('/api/ffmpeg/install', methods=['POST'])
def install_ffmpeg_api():
"""Intentar instalar FFmpeg automáticamente"""
try:
if check_ffmpeg():
return jsonify({'success': True, 'message': 'FFmpeg ya está disponible.'})
# Intentar instalar FFmpeg
success = install_ffmpeg()
if success:
return jsonify({'success': True, 'message': 'FFmpeg instalado correctamente.'})
else:
return jsonify({'success': False, 'message': 'No se pudo instalar FFmpeg automáticamente. Debes instalarlo manualmente.'})
except Exception as e:
return jsonify({'success': False, 'message': f'Error durante la instalación: {str(e)}'}), 500
@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)