hologramicon commited on
Commit
28cf732
·
verified ·
1 Parent(s): ef7cf4f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +50 -86
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
- # Creamos las carpetas necesarias aquí, al inicio del script.
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
- @app.route('/healthz')
23
- def health_check():
24
- """Ruta súper ligera para el chequeo de salud de Hugging Face."""
25
- return "OK", 200
26
-
27
- @app.route('/download/<filename>')
28
- def download_file(filename):
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
- final_video_name = f"video_{task_id}.webm"
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
- TASKS_STATUS[task_id] = {'status': 'processing', 'progress': 5, 'log': "Creando entorno de trabajo..."}
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) / total_videos) * 70) # La descarga ocupa del 5% al 75%
50
- log_msg = f"({i+1}/{total_videos}) Descargando: {url}"
51
- log.append(log_msg)
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
- # --- PASO 2: UNIÓN Y CONVERSIÓN CON FFMPEG ---
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
- log.append("\n¡Proceso completado con éxito!")
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 el comando:\nSTDOUT: {e.stdout}\nSTDERR: {e.stderr}"
102
- log.append(f"\n¡ERROR! El proceso falló.\n{error_details}")
103
- TASKS_STATUS[task_id] = {
104
- 'status': 'failed',
105
- 'log': "\n".join(log),
106
- 'error': error_details
107
- }
108
  except Exception as e:
109
- log.append(f"\n¡ERROR INESPERADO! {str(e)}")
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
- # --- ENDPOINTS DE LA API ---
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
- """Consulta el estado de un trabajo."""
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()