Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -2,154 +2,118 @@ import os
|
|
| 2 |
import uuid
|
| 3 |
import subprocess
|
| 4 |
import shutil
|
|
|
|
| 5 |
from flask import Flask, request, jsonify, send_from_directory
|
| 6 |
from threading import Thread
|
| 7 |
|
| 8 |
# --- CONFIGURACIÓN ---
|
| 9 |
OUTPUT_FOLDER = "processed_videos"
|
| 10 |
TEMP_FOLDER = "temp_processing"
|
|
|
|
| 11 |
|
| 12 |
-
#
|
| 13 |
-
# Se ejecutarán una vez cuando Gunicorn inicie la aplicación.
|
| 14 |
os.makedirs(OUTPUT_FOLDER, exist_ok=True)
|
| 15 |
os.makedirs(TEMP_FOLDER, exist_ok=True)
|
| 16 |
|
| 17 |
app = Flask(__name__, static_folder='static', static_url_path='')
|
| 18 |
-
|
| 19 |
-
# Diccionario en memoria para rastrear el estado de las tareas.
|
| 20 |
TASKS_STATUS = {}
|
| 21 |
|
| 22 |
-
|
| 23 |
-
def
|
| 24 |
-
"""
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
def video_processing_worker(task_id, urls):
|
| 31 |
-
"""
|
| 32 |
-
Función que se ejecuta en un hilo separado para no bloquear la API.
|
| 33 |
-
Descarga, une, convierte y limpia los videos.
|
| 34 |
-
"""
|
| 35 |
task_dir = os.path.join(TEMP_FOLDER, task_id)
|
| 36 |
-
|
| 37 |
-
final_video_path = os.path.join(OUTPUT_FOLDER, final_video_name)
|
| 38 |
log = []
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
try:
|
| 41 |
-
# --- PASO 0: PREPARACIÓN ---
|
| 42 |
os.makedirs(task_dir, exist_ok=True)
|
| 43 |
-
|
| 44 |
|
| 45 |
-
# --- PASO 1: DESCARGA DE VIDEOS ---
|
| 46 |
downloaded_files = []
|
| 47 |
-
total_videos = len(urls)
|
| 48 |
for i, url in enumerate(urls):
|
| 49 |
-
progress = 5 + int(((i + 1) /
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
TASKS_STATUS[task_id].update({'progress': progress, 'log': "\n".join(log)})
|
| 53 |
-
|
| 54 |
output_template = os.path.join(task_dir, f"{i:03d}.%(ext)s")
|
| 55 |
-
command = [
|
| 56 |
-
'yt-dlp',
|
| 57 |
-
'--no-check-certificate',
|
| 58 |
-
'-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
|
| 59 |
-
'--merge-output-format', 'mp4',
|
| 60 |
-
'-o', output_template,
|
| 61 |
-
url
|
| 62 |
-
]
|
| 63 |
subprocess.run(command, check=True, capture_output=True, text=True, encoding='utf-8')
|
| 64 |
|
| 65 |
created_file = next(f for f in os.listdir(task_dir) if f.startswith(f"{i:03d}"))
|
| 66 |
downloaded_files.append(os.path.join(task_dir, created_file))
|
| 67 |
|
| 68 |
-
|
| 69 |
-
log.append("\nUniendo videos y convirtiendo a WebM...")
|
| 70 |
-
TASKS_STATUS[task_id].update({'progress': 85, 'log': "\n".join(log)})
|
| 71 |
-
|
| 72 |
file_list_path = os.path.join(task_dir, "filelist.txt")
|
| 73 |
with open(file_list_path, 'w', encoding='utf-8') as f:
|
| 74 |
for file_path in downloaded_files:
|
| 75 |
f.write(f"file '{os.path.abspath(file_path)}'\n")
|
| 76 |
|
| 77 |
-
ffmpeg_command = [
|
| 78 |
-
'ffmpeg',
|
| 79 |
-
'-f', 'concat',
|
| 80 |
-
'-safe', '0',
|
| 81 |
-
'-i', file_list_path,
|
| 82 |
-
'-c:v', 'libvpx-vp9',
|
| 83 |
-
'-crf', '30',
|
| 84 |
-
'-b:v', '0',
|
| 85 |
-
'-c:a', 'libopus',
|
| 86 |
-
'-b:a', '128k',
|
| 87 |
-
'-y',
|
| 88 |
-
final_video_path
|
| 89 |
-
]
|
| 90 |
subprocess.run(ffmpeg_command, check=True, capture_output=True, text=True, encoding='utf-8')
|
| 91 |
|
| 92 |
-
|
| 93 |
-
TASKS_STATUS[task_id] = {
|
| 94 |
-
'status': 'completed',
|
| 95 |
-
'progress': 100,
|
| 96 |
-
'log': "\n".join(log),
|
| 97 |
-
'fileUrl': f"/download/{final_video_name}"
|
| 98 |
-
}
|
| 99 |
|
| 100 |
except subprocess.CalledProcessError as e:
|
| 101 |
-
error_details = f"Error en
|
| 102 |
-
|
| 103 |
-
TASKS_STATUS[task_id] = {
|
| 104 |
-
'status': 'failed',
|
| 105 |
-
'log': "\n".join(log),
|
| 106 |
-
'error': error_details
|
| 107 |
-
}
|
| 108 |
except Exception as e:
|
| 109 |
-
|
| 110 |
-
TASKS_STATUS[task_id] = {
|
| 111 |
-
'status': 'failed',
|
| 112 |
-
'log': "\n".join(log),
|
| 113 |
-
'error': str(e)
|
| 114 |
-
}
|
| 115 |
finally:
|
| 116 |
-
# --- PASO 3: LIMPIEZA ---
|
| 117 |
if os.path.exists(task_dir):
|
| 118 |
shutil.rmtree(task_dir)
|
| 119 |
|
| 120 |
-
# ---
|
| 121 |
-
|
| 122 |
@app.route('/')
|
| 123 |
def serve_index():
|
| 124 |
-
"""Sirve la página principal de la aplicación."""
|
| 125 |
return app.send_static_file('index.html')
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
@app.route('/api/process', methods=['POST'])
|
| 128 |
def start_processing():
|
| 129 |
-
"""Inicia un nuevo trabajo de procesamiento de video."""
|
| 130 |
urls = request.json.get('urls')
|
| 131 |
if not urls or not isinstance(urls, list):
|
| 132 |
return jsonify({'error': 'La lista de URLs es inválida.'}), 400
|
| 133 |
-
|
| 134 |
task_id = str(uuid.uuid4())
|
| 135 |
TASKS_STATUS[task_id] = {'status': 'queued', 'progress': 0, 'log': 'Tarea en cola...'}
|
| 136 |
|
| 137 |
thread = Thread(target=video_processing_worker, args=(task_id, urls))
|
| 138 |
thread.start()
|
| 139 |
-
|
| 140 |
return jsonify({'taskId': task_id})
|
| 141 |
|
| 142 |
@app.route('/api/status/<task_id>')
|
| 143 |
def get_status(task_id):
|
| 144 |
-
|
| 145 |
-
task = TASKS_STATUS.get(task_id)
|
| 146 |
-
if not task:
|
| 147 |
-
return jsonify({'error': 'Tarea no encontrada.'}), 404
|
| 148 |
-
return jsonify(task)
|
| 149 |
|
| 150 |
@app.route('/download/<filename>')
|
| 151 |
def download_file(filename):
|
| 152 |
-
"""Permite la descarga del video final."""
|
| 153 |
if ".." in filename or filename.startswith("/"):
|
| 154 |
return "Nombre de archivo no válido", 400
|
| 155 |
-
return send_from_directory(OUTPUT_FOLDER, filename, as_attachment=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import uuid
|
| 3 |
import subprocess
|
| 4 |
import shutil
|
| 5 |
+
import time
|
| 6 |
from flask import Flask, request, jsonify, send_from_directory
|
| 7 |
from threading import Thread
|
| 8 |
|
| 9 |
# --- CONFIGURACIÓN ---
|
| 10 |
OUTPUT_FOLDER = "processed_videos"
|
| 11 |
TEMP_FOLDER = "temp_processing"
|
| 12 |
+
MAX_FILE_AGE_SECONDS = 3600 # 1 hora en segundos
|
| 13 |
|
| 14 |
+
# Crea las carpetas necesarias al iniciar la app
|
|
|
|
| 15 |
os.makedirs(OUTPUT_FOLDER, exist_ok=True)
|
| 16 |
os.makedirs(TEMP_FOLDER, exist_ok=True)
|
| 17 |
|
| 18 |
app = Flask(__name__, static_folder='static', static_url_path='')
|
|
|
|
|
|
|
| 19 |
TASKS_STATUS = {}
|
| 20 |
|
| 21 |
+
# --- FUNCIÓN DE AUTOLIMPIEZA ---
|
| 22 |
+
def cleanup_old_files():
|
| 23 |
+
"""Borra archivos en OUTPUT_FOLDER más viejos que MAX_FILE_AGE_SECONDS."""
|
| 24 |
+
while True:
|
| 25 |
+
try:
|
| 26 |
+
for filename in os.listdir(OUTPUT_FOLDER):
|
| 27 |
+
file_path = os.path.join(OUTPUT_FOLDER, filename)
|
| 28 |
+
if os.path.isfile(file_path):
|
| 29 |
+
file_age = time.time() - os.path.getmtime(file_path)
|
| 30 |
+
if file_age > MAX_FILE_AGE_SECONDS:
|
| 31 |
+
os.remove(file_path)
|
| 32 |
+
print(f"Limpiado archivo antiguo: {filename}")
|
| 33 |
+
except Exception as e:
|
| 34 |
+
print(f"Error durante la limpieza: {e}")
|
| 35 |
+
time.sleep(600) # Revisa cada 10 minutos
|
| 36 |
+
|
| 37 |
+
# --- LÓGICA DE PROCESAMIENTO DE VIDEO ---
|
| 38 |
def video_processing_worker(task_id, urls):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
task_dir = os.path.join(TEMP_FOLDER, task_id)
|
| 40 |
+
final_video_path = os.path.join(OUTPUT_FOLDER, f"video_{task_id}.webm")
|
|
|
|
| 41 |
log = []
|
| 42 |
|
| 43 |
+
def update_status(status, progress, new_log=None):
|
| 44 |
+
if new_log:
|
| 45 |
+
log.append(new_log)
|
| 46 |
+
TASKS_STATUS[task_id] = {'status': status, 'progress': progress, 'log': "\n".join(log)}
|
| 47 |
+
|
| 48 |
try:
|
|
|
|
| 49 |
os.makedirs(task_dir, exist_ok=True)
|
| 50 |
+
update_status('processing', 5, "Entorno de trabajo creado.")
|
| 51 |
|
|
|
|
| 52 |
downloaded_files = []
|
|
|
|
| 53 |
for i, url in enumerate(urls):
|
| 54 |
+
progress = 5 + int(((i + 1) / len(urls)) * 70)
|
| 55 |
+
update_status('processing', progress, f"({i+1}/{len(urls)}) Descargando: {url}")
|
| 56 |
+
|
|
|
|
|
|
|
| 57 |
output_template = os.path.join(task_dir, f"{i:03d}.%(ext)s")
|
| 58 |
+
command = ['yt-dlp','-f','bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best','--merge-output-format','mp4','-o',output_template,url]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
subprocess.run(command, check=True, capture_output=True, text=True, encoding='utf-8')
|
| 60 |
|
| 61 |
created_file = next(f for f in os.listdir(task_dir) if f.startswith(f"{i:03d}"))
|
| 62 |
downloaded_files.append(os.path.join(task_dir, created_file))
|
| 63 |
|
| 64 |
+
update_status('processing', 85, "\nUniendo videos...")
|
|
|
|
|
|
|
|
|
|
| 65 |
file_list_path = os.path.join(task_dir, "filelist.txt")
|
| 66 |
with open(file_list_path, 'w', encoding='utf-8') as f:
|
| 67 |
for file_path in downloaded_files:
|
| 68 |
f.write(f"file '{os.path.abspath(file_path)}'\n")
|
| 69 |
|
| 70 |
+
ffmpeg_command = ['ffmpeg','-f','concat','-safe','0','-i',file_list_path,'-c:v','libvpx-vp9','-crf','30','-b:v','0','-c:a','libopus','-b:a','128k','-y',final_video_path]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
subprocess.run(ffmpeg_command, check=True, capture_output=True, text=True, encoding='utf-8')
|
| 72 |
|
| 73 |
+
update_status('completed', 100, "\n¡Proceso completado con éxito!")
|
| 74 |
+
TASKS_STATUS[task_id]['fileUrl'] = f"/download/{os.path.basename(final_video_path)}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
except subprocess.CalledProcessError as e:
|
| 77 |
+
error_details = f"Error en comando:\nSTDOUT: {e.stdout}\nSTDERR: {e.stderr}"
|
| 78 |
+
update_status('failed', 100, f"\n¡ERROR! El proceso falló.\n{error_details}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
except Exception as e:
|
| 80 |
+
update_status('failed', 100, f"\n¡ERROR INESPERADO! {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
finally:
|
|
|
|
| 82 |
if os.path.exists(task_dir):
|
| 83 |
shutil.rmtree(task_dir)
|
| 84 |
|
| 85 |
+
# --- RUTAS DE LA API (ENDPOINTS) ---
|
|
|
|
| 86 |
@app.route('/')
|
| 87 |
def serve_index():
|
|
|
|
| 88 |
return app.send_static_file('index.html')
|
| 89 |
|
| 90 |
+
@app.route('/healthz')
|
| 91 |
+
def health_check():
|
| 92 |
+
return "OK", 200
|
| 93 |
+
|
| 94 |
@app.route('/api/process', methods=['POST'])
|
| 95 |
def start_processing():
|
|
|
|
| 96 |
urls = request.json.get('urls')
|
| 97 |
if not urls or not isinstance(urls, list):
|
| 98 |
return jsonify({'error': 'La lista de URLs es inválida.'}), 400
|
| 99 |
+
|
| 100 |
task_id = str(uuid.uuid4())
|
| 101 |
TASKS_STATUS[task_id] = {'status': 'queued', 'progress': 0, 'log': 'Tarea en cola...'}
|
| 102 |
|
| 103 |
thread = Thread(target=video_processing_worker, args=(task_id, urls))
|
| 104 |
thread.start()
|
|
|
|
| 105 |
return jsonify({'taskId': task_id})
|
| 106 |
|
| 107 |
@app.route('/api/status/<task_id>')
|
| 108 |
def get_status(task_id):
|
| 109 |
+
return jsonify(TASKS_STATUS.get(task_id, {'error': 'Tarea no encontrada.'}))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
@app.route('/download/<filename>')
|
| 112 |
def download_file(filename):
|
|
|
|
| 113 |
if ".." in filename or filename.startswith("/"):
|
| 114 |
return "Nombre de archivo no válido", 400
|
| 115 |
+
return send_from_directory(OUTPUT_FOLDER, filename, as_attachment=True)
|
| 116 |
+
|
| 117 |
+
# --- INICIA EL HILO DE LIMPIEZA ---
|
| 118 |
+
cleanup_thread = Thread(target=cleanup_old_files, daemon=True)
|
| 119 |
+
cleanup_thread.start()
|