yt / app.py
hologramicon's picture
Update app.py
28cf732 verified
import os
import uuid
import subprocess
import shutil
import time
from flask import Flask, request, jsonify, send_from_directory
from threading import Thread
# --- CONFIGURACIÓN ---
OUTPUT_FOLDER = "processed_videos"
TEMP_FOLDER = "temp_processing"
MAX_FILE_AGE_SECONDS = 3600 # 1 hora en segundos
# Crea las carpetas necesarias al iniciar la app
os.makedirs(OUTPUT_FOLDER, exist_ok=True)
os.makedirs(TEMP_FOLDER, exist_ok=True)
app = Flask(__name__, static_folder='static', static_url_path='')
TASKS_STATUS = {}
# --- FUNCIÓN DE AUTOLIMPIEZA ---
def cleanup_old_files():
"""Borra archivos en OUTPUT_FOLDER más viejos que MAX_FILE_AGE_SECONDS."""
while True:
try:
for filename in os.listdir(OUTPUT_FOLDER):
file_path = os.path.join(OUTPUT_FOLDER, filename)
if os.path.isfile(file_path):
file_age = time.time() - os.path.getmtime(file_path)
if file_age > MAX_FILE_AGE_SECONDS:
os.remove(file_path)
print(f"Limpiado archivo antiguo: {filename}")
except Exception as e:
print(f"Error durante la limpieza: {e}")
time.sleep(600) # Revisa cada 10 minutos
# --- LÓGICA DE PROCESAMIENTO DE VIDEO ---
def video_processing_worker(task_id, urls):
task_dir = os.path.join(TEMP_FOLDER, task_id)
final_video_path = os.path.join(OUTPUT_FOLDER, f"video_{task_id}.webm")
log = []
def update_status(status, progress, new_log=None):
if new_log:
log.append(new_log)
TASKS_STATUS[task_id] = {'status': status, 'progress': progress, 'log': "\n".join(log)}
try:
os.makedirs(task_dir, exist_ok=True)
update_status('processing', 5, "Entorno de trabajo creado.")
downloaded_files = []
for i, url in enumerate(urls):
progress = 5 + int(((i + 1) / len(urls)) * 70)
update_status('processing', progress, f"({i+1}/{len(urls)}) Descargando: {url}")
output_template = os.path.join(task_dir, f"{i:03d}.%(ext)s")
command = ['yt-dlp','-f','bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best','--merge-output-format','mp4','-o',output_template,url]
subprocess.run(command, check=True, capture_output=True, text=True, encoding='utf-8')
created_file = next(f for f in os.listdir(task_dir) if f.startswith(f"{i:03d}"))
downloaded_files.append(os.path.join(task_dir, created_file))
update_status('processing', 85, "\nUniendo videos...")
file_list_path = os.path.join(task_dir, "filelist.txt")
with open(file_list_path, 'w', encoding='utf-8') as f:
for file_path in downloaded_files:
f.write(f"file '{os.path.abspath(file_path)}'\n")
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]
subprocess.run(ffmpeg_command, check=True, capture_output=True, text=True, encoding='utf-8')
update_status('completed', 100, "\n¡Proceso completado con éxito!")
TASKS_STATUS[task_id]['fileUrl'] = f"/download/{os.path.basename(final_video_path)}"
except subprocess.CalledProcessError as e:
error_details = f"Error en comando:\nSTDOUT: {e.stdout}\nSTDERR: {e.stderr}"
update_status('failed', 100, f"\n¡ERROR! El proceso falló.\n{error_details}")
except Exception as e:
update_status('failed', 100, f"\n¡ERROR INESPERADO! {str(e)}")
finally:
if os.path.exists(task_dir):
shutil.rmtree(task_dir)
# --- RUTAS DE LA API (ENDPOINTS) ---
@app.route('/')
def serve_index():
return app.send_static_file('index.html')
@app.route('/healthz')
def health_check():
return "OK", 200
@app.route('/api/process', methods=['POST'])
def start_processing():
urls = request.json.get('urls')
if not urls or not isinstance(urls, list):
return jsonify({'error': 'La lista de URLs es inválida.'}), 400
task_id = str(uuid.uuid4())
TASKS_STATUS[task_id] = {'status': 'queued', 'progress': 0, 'log': 'Tarea en cola...'}
thread = Thread(target=video_processing_worker, args=(task_id, urls))
thread.start()
return jsonify({'taskId': task_id})
@app.route('/api/status/<task_id>')
def get_status(task_id):
return jsonify(TASKS_STATUS.get(task_id, {'error': 'Tarea no encontrada.'}))
@app.route('/download/<filename>')
def download_file(filename):
if ".." in filename or filename.startswith("/"):
return "Nombre de archivo no válido", 400
return send_from_directory(OUTPUT_FOLDER, filename, as_attachment=True)
# --- INICIA EL HILO DE LIMPIEZA ---
cleanup_thread = Thread(target=cleanup_old_files, daemon=True)
cleanup_thread.start()