CineMax commited on
Commit
c1b40f2
Β·
verified Β·
1 Parent(s): a5b01ec

Update hug.py

Browse files
Files changed (1) hide show
  1. hug.py +345 -257
hug.py CHANGED
@@ -5,13 +5,14 @@ import threading
5
  import shutil
6
  import re
7
  import json
 
 
 
8
  import tkinter as tk
9
  from tkinter import ttk, filedialog, messagebox
10
  from pathlib import Path
11
 
12
  # ─── IMPORTS OPCIONALES / DEFERIDOS ─────────────────────────
13
- # Importamos sys solo cuando se necesita para evitar errores tempranos en entornos extraΓ±os,
14
- # aunque es parte de la librerΓ­a estΓ‘ndar.
15
  def get_sys():
16
  import sys
17
  return sys
@@ -31,16 +32,13 @@ def elevate():
31
  )
32
  sys.exit()
33
 
34
- # Descomenta la siguiente lΓ­nea si siempre deseas ejecutar como Administrador
35
- # elevate()
36
-
37
  # ─── VERIFICACIΓ“N DE LIBRERÍA HF ────────────────────────────
38
  HF_OK = False
39
  try:
40
- from huggingface_hub import HfApi, login
41
  HF_OK = True
42
  except ImportError:
43
- pass # Se manejarΓ‘ en la UI si se intenta usar sin instalar
44
 
45
  # ─── ESTILOS Y COLORES ──────────────────────────────────────
46
  BG = "#0f172a"
@@ -60,13 +58,29 @@ FONT_TITLE= ("Segoe UI", 14, "bold")
60
  # ─── FUNCIONES UTILITARIAS ──────────────────────────────────
61
  def safe_name(s, maxlen=120):
62
  if not s: return "video"
63
- # Reemplaza caracteres invΓ‘lidos en nombres de archivo y rutas
64
  return re.sub(r'[<>:"/\\|?*\x00-\x1f]', '_', s).strip()[:maxlen]
65
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  def fmt_dur(secs):
67
  s = int(secs)
68
  return f"{s//3600:02d}:{(s%3600)//60:02d}:{s%60:02d}"
69
 
 
 
 
 
 
70
  def probe(source):
71
  sys = get_sys()
72
  info = {"audio": [], "subs": [], "duration": 0.0, "title": ""}
@@ -133,80 +147,25 @@ def run_ffmpeg(cmd, total, log_cb, progress_cb, label):
133
  log_cb(f" βœ— ffmpeg: {e}")
134
  return False
135
 
136
- def hf_upload(token, repo_id, folder_path, log_cb, progress_cb):
137
- if not HF_OK:
138
- log_cb(" βœ— Error: LibrerΓ­a huggingface_hub no instalada.")
139
- return False
140
-
141
- try:
142
- api = HfApi()
143
- # Verificar token primero
144
- try:
145
- user_info = api.whoami(token=token)
146
- log_cb(f" βœ“ Conectado como: {user_info['name']}")
147
- except Exception as auth_err:
148
- log_cb(f" βœ— Error de autenticaciΓ³n: Token invΓ‘lido o sin permisos.")
149
- return False
150
-
151
- # Crear/Subir
152
- api.create_repo(repo_id=repo_id, repo_type="model", private=True, exist_ok=True, token=token)
153
-
154
- files = list(Path(folder_path).iterdir())
155
- total_files = len(files)
156
- for i, f in enumerate(files):
157
- if f.is_file():
158
- # Usamos el nombre de la carpeta padre (que es el nombre del video) en la ruta remota
159
- parent_name = Path(folder_path).name
160
- rpath = f"videos/{parent_name}/{f.name}"
161
- log_cb(f" ↑ {f.name}")
162
- progress_cb(int((i/total_files)*100), f"Subiendo {i+1}/{total_files}")
163
- api.upload_file(
164
- path_or_fileobj=str(f),
165
- path_in_repo=rpath,
166
- repo_id=repo_id,
167
- repo_type="model",
168
- token=token
169
- )
170
- progress_cb(100, "Subida completa")
171
- log_cb(f" βœ“ Subido β†’ {repo_id}")
172
- return True
173
- except Exception as e:
174
- log_cb(f" βœ— Upload error: {e}")
175
- return False
176
-
177
  def process_video(token, repo_id, source, is_url, mode,
178
  audio_idx, gen_single, extract_sub, sub_idx,
179
- delete_local, manual_name, log_cb, progress_cb, done_cb):
180
  try:
181
  extra = NET_ARGS if is_url else []
182
- log_cb("⟳ Analizando fuente…")
183
  info = probe(source)
184
  dur = info["duration"] if info["duration"] > 0 else 1
185
- log_cb(f" βœ“ {fmt_dur(dur)} | {len(info['audio'])} audio | {len(info['subs'])} sub")
186
-
187
- # LΓ“GICA DE NOMBROS: Prioridad Manual > TΓ­tulo Metadata > Nombre Archivo
188
- base = ""
189
- if manual_name and manual_name.strip():
190
- base = manual_name.strip()
191
- log_cb(f" β„Ή Usando nombre manual: '{base}'")
192
- elif info["title"]:
193
- base = info["title"]
194
- log_cb(f" β„Ή Usando nombre de metadata: '{base}'")
195
- else:
196
- base = Path(source).stem or "video"
197
- log_cb(f" β„Ή Usando nombre de archivo: '{base}'")
198
-
199
- fname = safe_name(base)
200
 
201
  sys = get_sys()
202
  script_dir = Path(sys.argv[0]).parent.resolve()
203
  output_root = script_dir / "Videos_Procesados"
204
  output_root.mkdir(exist_ok=True)
205
 
206
- tmp = output_root / fname
207
- tmp.mkdir(exist_ok=True)
 
208
 
209
- out_mp4 = tmp / f"{fname}.mp4"
210
 
211
  cmd = ["ffmpeg", "-y"] + extra + ["-i", source, "-map", "0:v:0"]
212
  for i in range(len(info["audio"])):
@@ -231,41 +190,58 @@ def process_video(token, repo_id, source, is_url, mode,
231
  ok = run_ffmpeg(cmd, dur, log_cb, progress_cb, "Convirtiendo")
232
  if not ok:
233
  log_cb("βœ— ConversiΓ³n fallΓ³")
234
- if delete_local:
235
- shutil.rmtree(tmp, ignore_errors=True)
236
  done_cb(False)
237
  return
238
 
239
  progress_cb(100, "ConversiΓ³n OK")
240
- log_cb(" βœ“ ConversiΓ³n OK")
241
 
242
  if gen_single and info["audio"] and audio_idx < len(info["audio"]):
243
- sp = tmp / f"{fname}_aud{audio_idx}.mp4"
244
  sc = ["ffmpeg", "-y", "-i", str(out_mp4),
245
  "-map", "0:v:0", "-map", f"0:a:{audio_idx}", "-c", "copy", str(sp)]
246
- run_ffmpeg(sc, dur, log_cb, progress_cb, "Audio individual")
247
- log_cb(" βœ“ Audio individual extraΓ­do")
248
 
249
  if extract_sub and info["subs"] and sub_idx < len(info["subs"]):
250
- vtt = tmp / f"{fname}_sub{sub_idx}.vtt"
251
  sc = ["ffmpeg", "-y"] + extra + [
252
  "-i", source, "-map", f"0:s:{sub_idx}", "-c:s", "webvtt", str(vtt)]
253
- try:
254
- subprocess.run(sc, capture_output=True, check=True, timeout=120)
255
- log_cb(" βœ“ SubtΓ­tulo extraΓ­do")
256
- except Exception as e:
257
- log_cb(f" ⚠ sub error: {e}")
258
 
259
  log_cb(f"☁ Subiendo a HuggingFace β†’ {repo_id}")
260
- ok2 = hf_upload(token, repo_id, str(tmp), log_cb, progress_cb)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
 
262
  if delete_local:
263
  shutil.rmtree(tmp, ignore_errors=True)
264
- log_cb(" βœ“ Archivos locales borrados")
265
  else:
266
- log_cb(f" β„Ή Archivos guardados en: {tmp}")
 
 
 
 
 
267
 
268
- done_cb(ok2)
269
 
270
  except Exception as e:
271
  import traceback
@@ -275,12 +251,15 @@ def process_video(token, repo_id, source, is_url, mode,
275
  class App(tk.Tk):
276
  def __init__(self):
277
  super().__init__()
278
- self.title("Video Converter Pro")
279
- self.geometry("1000x720")
280
  self.configure(bg=BG)
281
  self.resizable(True, True)
282
- self._repos = []
283
- self._info = None
 
 
 
284
  self._build()
285
 
286
  def _build(self):
@@ -290,42 +269,37 @@ class App(tk.Tk):
290
  style.configure("TFrame", background=BG)
291
  style.configure("Card.TFrame", background=CARD_BG)
292
  style.configure("TLabel", background=BG, foreground=FG, font=FONT_MAIN)
293
- style.configure("Card.TLabel", background=CARD_BG, foreground=FG, font=FONT_MAIN)
294
  style.configure("Header.TLabel", background=BG, foreground=FG, font=FONT_TITLE)
295
- style.configure("Dim.TLabel", background=BG, foreground=FG2, font=FONT_MAIN)
296
- style.configure("Accent.TButton", background=ACCENT, foreground="white", font=FONT_BOLD, padding=10, borderwidth=0)
297
  style.map("Accent.TButton", background=[("active", ACCENT_H)])
298
- style.configure("TCombobox", fieldbackground=CARD_BG, background=BG, foreground=FG, arrowcolor=FG2, borderwidth=1)
299
- style.map("TCombobox", fieldbackground=[("readonly", CARD_BG)], foreground=[("readonly", FG)])
300
  style.configure("Horizontal.TProgressbar", troughcolor=BG, background=ACCENT, thickness=8)
301
 
302
- main_container = tk.Frame(self, bg=BG)
303
- main_container.pack(fill="both", expand=True, padx=20, pady=20)
304
 
305
- header_frame = tk.Frame(main_container, bg=BG)
306
- header_frame.pack(fill="x", pady=(0, 20))
307
- tk.Label(header_frame, text="VIDEO CONVERTER PRO", bg=BG, fg=ACCENT, font=FONT_TITLE).pack(side="left")
308
- self._lbl_status = tk.Label(header_frame, text="● Listo", bg=BG, fg=GREEN, font=FONT_MAIN)
309
  self._lbl_status.pack(side="right")
310
 
311
- body_grid = tk.Frame(main_container, bg=BG)
312
- body_grid.pack(fill="both", expand=True)
313
- body_grid.columnconfigure(0, weight=1, uniform="group1")
314
- body_grid.columnconfigure(1, weight=1, uniform="group1")
315
- body_grid.rowconfigure(0, weight=1)
316
-
317
- left_panel = tk.Frame(body_grid, bg=CARD_BG, padx=15, pady=15)
318
- left_panel.grid(row=0, column=0, sticky="nsew", padx=(0, 10))
319
 
320
- self._build_hf_section(left_panel)
321
- self._build_options_section(left_panel) # Se actualizΓ³ para incluir nombre manual
322
- self._build_tracks_section(left_panel)
 
 
323
 
324
- right_panel = tk.Frame(body_grid, bg=CARD_BG, padx=15, pady=15)
325
- right_panel.grid(row=0, column=1, sticky="nsew", padx=(10, 0))
326
-
327
- self._build_source_section(right_panel)
328
- self._build_log_section(right_panel)
329
 
330
  def _create_section(self, parent, title):
331
  f = tk.Frame(parent, bg=CARD_BG, pady=5)
@@ -339,143 +313,240 @@ class App(tk.Tk):
339
  s = self._create_section(parent, "HuggingFace")
340
  tk.Label(s, text="Token:", bg=CARD_BG, fg=FG2).pack(anchor="w")
341
  self._hf_token = tk.StringVar()
342
- tk.Entry(s, textvariable=self._hf_token, show="*", bg=BG, fg=FG, insertbackground=FG, font=FONT_MAIN, relief="flat").pack(fill="x", ipady=4, pady=(0, 10))
343
 
344
- f_repo = tk.Frame(s, bg=CARD_BG)
345
- f_repo.pack(fill="x", pady=(0, 5))
346
- tk.Label(f_repo, text="Repositorio:", bg=CARD_BG, fg=FG2).pack(anchor="w")
347
- cb_frame = tk.Frame(f_repo, bg=CARD_BG)
348
- cb_frame.pack(fill="x")
 
 
 
 
 
 
349
 
350
- # Habilitar escritura en el combobox para permitir nombres manuales de repo tambiΓ©n si se quisiera,
351
- # pero mantenemos 'readonly' por defecto y se edita si el usuario lo desea, o mejor usamos Entry normal para el repo?
352
- # Mantendremos el Combobox pero permitiremos escribir si el usuario configura 'normal' aunque lo puse readonly.
353
- # Para permitir escribir el repo manualmente si no estΓ‘ en la lista, lo cambiamos a estado normal o usamos un trick.
354
- # Por seguridad lo dejaremos readonly pero con opciΓ³n de refrescar.
 
 
 
 
355
 
356
- self._repo_cb = ttk.Combobox(cb_frame, state="readonly", font=FONT_MAIN)
357
- self._repo_cb.pack(side="left", fill="x", expand=True)
 
 
 
 
 
 
 
 
 
 
 
358
 
359
- # BotΓ³n para cargar repos
360
- btn_refresh = tk.Button(cb_frame, text="↻", bg=BG, fg=ACCENT, bd=0, font=FONT_BOLD, cursor="hand2", command=self._load_repos)
361
- btn_refresh.pack(side="right", padx=(5, 0))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
 
363
- # BotΓ³n para permitir editar el repo manualmente (Toggle)
364
- self._repo_editable = False
365
- self._btn_edit_repo = tk.Button(cb_frame, text="✎", bg=BG, fg=FG2, bd=0, font=FONT_BOLD, cursor="hand2", command=self._toggle_repo_edit)
366
- self._btn_edit_repo.pack(side="right", padx=(2,0))
367
-
368
- def _toggle_repo_edit(self):
369
- if self._repo_editable:
370
- self._repo_cb.configure(state="readonly")
371
- self._btn_edit_repo.configure(fg=FG2)
372
- self._repo_editable = False
373
- else:
374
- self._repo_cb.configure(state="normal")
375
- self._repo_cb.focus()
376
- self._btn_edit_repo.configure(fg=ACCENT)
377
- self._repo_editable = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
 
379
  def _build_options_section(self, parent):
380
  s = self._create_section(parent, "ConfiguraciΓ³n")
381
-
382
- # --- NUEVO: NOMBRE MANUAL ---
383
- tk.Label(s, text="Nombre de salida (Opcional):", bg=CARD_BG, fg=FG2).pack(anchor="w")
384
  self._manual_name = tk.StringVar()
385
- # Placeholder o hint
386
- e_name = tk.Entry(s, textvariable=self._manual_name, bg=BG, fg=FG, insertbackground=FG, font=FONT_MAIN, relief="flat")
387
- e_name.pack(fill="x", ipady=4, pady=(0, 10))
388
- # -----------------------------
389
 
390
  self._mode = tk.StringVar(value="Copy + MP3")
 
 
391
  for m in ["Copy + MP3", "Copy + FLAC", "H264 1080p"]:
392
- ttk.Radiobutton(s, text=m, variable=self._mode, value=m).pack(anchor="w", pady=2)
393
 
394
  self._gen_single = tk.BooleanVar(value=False)
395
  self._ext_sub = tk.BooleanVar(value=False)
396
  self._del_local = tk.BooleanVar(value=True)
397
 
398
- opts_frame = tk.Frame(s, bg=CARD_BG)
399
- opts_frame.pack(fill="x", pady=(10, 0))
400
- ttk.Checkbutton(opts_frame, text="Audio individual", variable=self._gen_single).pack(anchor="w")
401
- ttk.Checkbutton(opts_frame, text="Extraer subtΓ­tulos", variable=self._ext_sub).pack(anchor="w")
402
- ttk.Checkbutton(opts_frame, text="Borrar locales al terminar", variable=self._del_local).pack(anchor="w")
403
 
404
  def _build_tracks_section(self, parent):
405
- s = self._create_section(parent, "Pistas Detectadas")
406
- tk.Label(s, text="Pista Audio:", bg=CARD_BG, fg=FG2).pack(anchor="w")
407
- self._aud_cb = ttk.Combobox(s, state="readonly", font=FONT_MAIN)
408
- self._aud_cb.pack(fill="x", pady=(0, 8))
409
- tk.Label(s, text="Pista SubtΓ­tulo:", bg=CARD_BG, fg=FG2).pack(anchor="w")
410
- self._sub_cb = ttk.Combobox(s, state="readonly", font=FONT_MAIN)
411
- self._sub_cb.pack(fill="x")
 
 
412
 
413
  def _build_source_section(self, parent):
414
  s = self._create_section(parent, "Fuente")
415
 
 
416
  tabs = tk.Frame(s, bg=CARD_BG)
417
  tabs.pack(fill="x", pady=(0, 10))
418
- self._src_mode = tk.StringVar(value="file")
419
 
420
- btn_file = tk.Button(tabs, text="Archivo", bg=ACCENT, fg="white", bd=0, padx=10, pady=5, font=FONT_MAIN, command=lambda: self._switch_src("file", btn_file, btn_url))
421
- btn_file.pack(side="left")
422
- btn_url = tk.Button(tabs, text="URL", bg=BG, fg=FG2, bd=0, padx=10, pady=5, font=FONT_MAIN, command=lambda: self._switch_src("url", btn_file, btn_url))
423
- btn_url.pack(side="left", padx=5)
424
 
 
425
  self._file_frame = tk.Frame(s, bg=CARD_BG)
426
- self._file_frame.pack(fill="x")
427
  self._file_var = tk.StringVar()
428
  tk.Entry(self._file_frame, textvariable=self._file_var, bg=BG, fg=FG, insertbackground=FG, font=FONT_MAIN, relief="flat").pack(side="left", fill="x", expand=True, ipady=4, padx=(0, 5))
429
  tk.Button(self._file_frame, text="Buscar", bg=BG, fg=ACCENT, bd=0, padx=8, pady=4, font=FONT_MAIN, command=self._browse).pack(side="right")
430
 
 
431
  self._url_frame = tk.Frame(s, bg=CARD_BG)
432
- self._url_var = tk.StringVar()
433
- tk.Entry(self._url_frame, textvariable=self._url_var, bg=BG, fg=FG, insertbackground=FG, font=FONT_MAIN, relief="flat").pack(fill="x", ipady=4)
434
-
435
- self._switch_src("file", btn_file, btn_url)
 
436
 
 
437
  self._lbl_info = tk.Label(s, text="Sin anΓ‘lisis", bg=BG, fg=FG2, font=FONT_MAIN, anchor="w", padx=10, pady=5)
438
  self._lbl_info.pack(fill="x", pady=(10, 0))
 
 
 
 
 
 
 
439
 
440
- btn_frame = tk.Frame(s, bg=CARD_BG)
441
- btn_frame.pack(fill="x", pady=(10, 0))
442
- tk.Button(btn_frame, text="ANALIZAR", bg=BG, fg=ACCENT, bd=0, padx=10, pady=5, font=FONT_BOLD, cursor="hand2", command=self._do_analyze).pack(side="left")
443
- self._btn_proc = tk.Button(btn_frame, text="PROCESAR Y SUBIR", bg=ACCENT, fg="white", bd=0, padx=10, pady=5, font=FONT_BOLD, cursor="hand2", command=self._do_process)
444
- self._btn_proc.pack(side="right")
445
 
446
  def _build_log_section(self, parent):
447
  s = self._create_section(parent, "Progreso")
448
- self._lbl_prog = tk.Label(s, text="Esperando...", bg=CARD_BG, fg=FG2, font=FONT_MAIN, anchor="w")
449
  self._lbl_prog.pack(fill="x")
450
  self._pbar = ttk.Progressbar(s, mode="determinate", style="Horizontal.TProgressbar")
451
  self._pbar.pack(fill="x", pady=(5, 10))
452
 
453
  log_f = tk.Frame(s, bg=BG, padx=1, pady=1)
454
  log_f.pack(fill="both", expand=True)
455
- self._log_txt = tk.Text(log_f, bg=BG, fg="#0ea5e9", font=("Consolas", 9), bd=0, relief="flat", state="disabled", wrap="word")
456
  sb = ttk.Scrollbar(log_f, command=self._log_txt.yview)
457
  self._log_txt.configure(yscrollcommand=sb.set)
458
  sb.pack(side="right", fill="y")
459
  self._log_txt.pack(fill="both", expand=True)
460
 
461
- def _switch_src(self, mode, btn_file, btn_url):
462
  self._src_mode.set(mode)
463
  if mode == "file":
464
  self._file_frame.pack(fill="x")
465
  self._url_frame.pack_forget()
466
- btn_file.configure(bg=ACCENT, fg="white")
467
- btn_url.configure(bg=BG, fg=FG2)
468
  else:
469
  self._url_frame.pack(fill="x")
470
  self._file_frame.pack_forget()
471
- btn_url.configure(bg=ACCENT, fg="white")
472
- btn_file.configure(bg=BG, fg=FG2)
473
 
474
  def _browse(self):
475
  p = filedialog.askopenfilename(filetypes=[("Video", "*.mp4 *.mkv *.avi *.mov *.ts"), ("Todos", "*.*")])
476
  if p:
477
  self._file_var.set(p)
478
 
 
 
 
 
 
 
 
 
479
  def _log(self, msg):
480
  self._log_txt.configure(state="normal")
481
  self._log_txt.insert("end", msg + "\n")
@@ -487,54 +558,32 @@ class App(tk.Tk):
487
  self._lbl_prog.configure(text=label)
488
  self.update_idletasks()
489
 
490
- def _get_source(self):
491
- if self._src_mode.get() == "file":
492
- return self._file_var.get().strip(), False
493
- return self._url_var.get().strip(), True
494
-
495
  def _load_repos(self):
496
- token = self._hf_token.get().strip()
497
- if not token:
498
- messagebox.showwarning("Error", "Por favor ingresa un Token primero.")
499
- return
500
-
501
- if not HF_OK:
502
- messagebox.showerror("Error", "La librerΓ­a 'huggingface_hub' no estΓ‘ instalada.")
503
- return
504
-
505
  self._log("⟳ Cargando repositorios...")
506
-
507
  def _go():
508
  try:
509
  api = HfApi()
510
- # Verificar credenciales y obtener usuario
511
- user_info = api.whoami(token=token)
512
- name = user_info.get('name', '')
513
-
514
- # Listar modelos
515
- models = api.list_models(author=name, token=token, limit=100)
516
- repos = [m.modelId for m in models]
517
-
518
- self._repos = repos
519
  self.after(0, lambda: self._repo_cb.configure(values=repos))
520
- if repos:
521
- self.after(0, lambda: self._repo_cb.set(repos[0]))
522
- self._log(f"βœ“ {len(repos)} repositorios encontrados para '{name}'.")
523
- except Exception as e:
524
- self._log(f"βœ— Error: {e}")
525
- self.after(0, lambda: messagebox.showerror("Error HF", f"No se pudieron cargar repos.\nVerifica que el token tenga permisos de 'Write'.\n\nError: {e}"))
526
-
527
  threading.Thread(target=_go, daemon=True).start()
528
 
529
  def _do_analyze(self):
530
- source, _ = self._get_source()
531
- if not source:
532
- messagebox.showwarning("Error", "Selecciona una fuente.")
533
  return
 
534
  def _go():
535
  self._log("⟳ Analizando...")
 
 
 
536
  info = probe(source)
537
- self._info = info
538
  ac = [f"[{t['idx']}] {t['lang']} Β· {t['codec']}" for t in info["audio"]]
539
  sc = [f"[{t['idx']}] {t['lang']} Β· {t['codec']}" for t in info["subs"]]
540
 
@@ -543,59 +592,98 @@ class App(tk.Tk):
543
  if ac: self.after(0, lambda: self._aud_cb.set(ac[0]))
544
  if sc: self.after(0, lambda: self._sub_cb.set(sc[0]))
545
 
546
- txt = f"βœ“ {info['title'] or 'Video'} | {fmt_dur(info['duration'])} | {len(ac)} aud | {len(sc)} sub"
 
 
 
 
547
  self.after(0, lambda: self._lbl_info.configure(text=txt, fg=GREEN))
548
  self._log(txt)
 
 
 
549
  threading.Thread(target=_go, daemon=True).start()
550
 
551
  def _do_process(self):
552
- if not HF_OK:
553
- messagebox.showerror("Error", "Falta la librerΓ­a 'huggingface_hub'. No se puede procesar.")
554
- return
555
 
556
- token = self._hf_token.get().strip()
557
- repo_id = self._repo_cb.get().strip()
558
- source, is_url = self._get_source()
559
- manual_name = self._manual_name.get().strip()
560
 
561
- if not token: return messagebox.showwarning("Error", "Ingresa el Token.")
562
- if not repo_id: return messagebox.showwarning("Error", "Selecciona o escribe un Repositorio.")
563
- if not source: return messagebox.showwarning("Error", "Selecciona una Fuente.")
564
-
565
- ai = 0
566
- si = 0
567
- try:
568
- if self._aud_cb.get(): ai = int(self._aud_cb.get().split("]")[0].strip("["))
569
- except: pass
570
- try:
571
- if self._sub_cb.get(): si = int(self._sub_cb.get().split("]")[0].strip("["))
572
- except: pass
573
 
574
  self._btn_proc.configure(state="disabled", bg=FG2)
575
- self._lbl_status.configure(text="● Procesando…", fg=YELLOW)
576
  self._log_txt.configure(state="normal")
577
  self._log_txt.delete("1.0", "end")
578
  self._log_txt.configure(state="disabled")
579
- self._pbar["value"] = 0
 
580
 
581
- def _done(ok):
 
582
  self._btn_proc.configure(state="normal", bg=ACCENT)
583
- if ok:
584
- self._lbl_status.configure(text="● Completado", fg=GREEN)
585
- self._log("βœ“ COMPLETADO")
586
- self._set_progress(100, "Completado")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
587
  else:
588
- self._lbl_status.configure(text="● Error", fg=RED)
589
- self._log("βœ— FALLΓ“")
 
 
 
 
 
 
 
 
 
 
 
 
590
 
591
  threading.Thread(
592
  target=process_video,
593
- args=(token, repo_id, source, is_url, self._mode.get(),
594
- ai, self._gen_single.get(), self._ext_sub.get(), si,
595
- self._del_local.get(), manual_name, # Paso el nombre manual
596
  lambda m: self.after(0, self._log, m),
597
  lambda p, l: self.after(0, self._set_progress, p, l),
598
- lambda ok: self.after(0, _done, ok)),
599
  daemon=True
600
  ).start()
601
 
 
5
  import shutil
6
  import re
7
  import json
8
+ import uuid
9
+ import urllib.request
10
+ import urllib.parse
11
  import tkinter as tk
12
  from tkinter import ttk, filedialog, messagebox
13
  from pathlib import Path
14
 
15
  # ─── IMPORTS OPCIONALES / DEFERIDOS ─────────────────────────
 
 
16
  def get_sys():
17
  import sys
18
  return sys
 
32
  )
33
  sys.exit()
34
 
 
 
 
35
  # ─── VERIFICACIΓ“N DE LIBRERÍA HF ────────────────────────────
36
  HF_OK = False
37
  try:
38
+ from huggingface_hub import HfApi
39
  HF_OK = True
40
  except ImportError:
41
+ pass
42
 
43
  # ─── ESTILOS Y COLORES ──────────────────────────────────────
44
  BG = "#0f172a"
 
58
  # ─── FUNCIONES UTILITARIAS ──────────────────────────────────
59
  def safe_name(s, maxlen=120):
60
  if not s: return "video"
 
61
  return re.sub(r'[<>:"/\\|?*\x00-\x1f]', '_', s).strip()[:maxlen]
62
 
63
+ def to_leet(text):
64
+ if not text: return "video"
65
+ rep = {
66
+ 'a': '4', 'A': '4',
67
+ 'i': '1', 'I': '1',
68
+ 't': '7', 'T': '7',
69
+ 'o': '0', 'O': '0',
70
+ 'e': '3', 'E': '3'
71
+ }
72
+ res = "".join(rep.get(c, c) for c in text)
73
+ return safe_name(res)
74
+
75
  def fmt_dur(secs):
76
  s = int(secs)
77
  return f"{s//3600:02d}:{(s%3600)//60:02d}:{s%60:02d}"
78
 
79
+ def fetch_tmdb(url):
80
+ req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
81
+ with urllib.request.urlopen(req) as resp:
82
+ return json.loads(resp.read().decode())
83
+
84
  def probe(source):
85
  sys = get_sys()
86
  info = {"audio": [], "subs": [], "duration": 0.0, "title": ""}
 
147
  log_cb(f" βœ— ffmpeg: {e}")
148
  return False
149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  def process_video(token, repo_id, source, is_url, mode,
151
  audio_idx, gen_single, extract_sub, sub_idx,
152
+ delete_local, folder_name, file_name, log_cb, progress_cb, done_cb):
153
  try:
154
  extra = NET_ARGS if is_url else []
155
+ log_cb(f"⟳ Analizando fuente: {source[:50]}...")
156
  info = probe(source)
157
  dur = info["duration"] if info["duration"] > 0 else 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
  sys = get_sys()
160
  script_dir = Path(sys.argv[0]).parent.resolve()
161
  output_root = script_dir / "Videos_Procesados"
162
  output_root.mkdir(exist_ok=True)
163
 
164
+ uid = str(uuid.uuid4())[:8]
165
+ tmp = output_root / f"temp_{uid}"
166
+ tmp.mkdir(exist_ok=True, parents=True)
167
 
168
+ out_mp4 = tmp / f"{file_name}.mp4"
169
 
170
  cmd = ["ffmpeg", "-y"] + extra + ["-i", source, "-map", "0:v:0"]
171
  for i in range(len(info["audio"])):
 
190
  ok = run_ffmpeg(cmd, dur, log_cb, progress_cb, "Convirtiendo")
191
  if not ok:
192
  log_cb("βœ— ConversiΓ³n fallΓ³")
193
+ if delete_local: shutil.rmtree(tmp, ignore_errors=True)
 
194
  done_cb(False)
195
  return
196
 
197
  progress_cb(100, "ConversiΓ³n OK")
 
198
 
199
  if gen_single and info["audio"] and audio_idx < len(info["audio"]):
200
+ sp = tmp / f"{file_name}_aud{audio_idx}.mp4"
201
  sc = ["ffmpeg", "-y", "-i", str(out_mp4),
202
  "-map", "0:v:0", "-map", f"0:a:{audio_idx}", "-c", "copy", str(sp)]
203
+ run_ffmpeg(sc, dur, log_cb, progress_cb, "Audio extraΓ­do")
 
204
 
205
  if extract_sub and info["subs"] and sub_idx < len(info["subs"]):
206
+ vtt = tmp / f"{file_name}_sub{sub_idx}.vtt"
207
  sc = ["ffmpeg", "-y"] + extra + [
208
  "-i", source, "-map", f"0:s:{sub_idx}", "-c:s", "webvtt", str(vtt)]
209
+ subprocess.run(sc, capture_output=True, check=False)
 
 
 
 
210
 
211
  log_cb(f"☁ Subiendo a HuggingFace β†’ {repo_id}")
212
+ if HF_OK:
213
+ api = HfApi()
214
+ files = list(tmp.iterdir())
215
+ for i, f in enumerate(files):
216
+ if f.is_file():
217
+ rpath = f"videos/{folder_name}/{f.name}"
218
+ log_cb(f" ↑ {f.name} -> {rpath}")
219
+ progress_cb(int((i/len(files))*100), f"Subiendo {i+1}/{len(files)}")
220
+ try:
221
+ api.upload_file(
222
+ path_or_fileobj=str(f),
223
+ path_in_repo=rpath,
224
+ repo_id=repo_id,
225
+ repo_type="model",
226
+ token=token
227
+ )
228
+ except Exception as e:
229
+ log_cb(f" βœ— Error subiendo {f.name}: {e}")
230
+ else:
231
+ log_cb(" βœ— HuggingFace no disponible.")
232
 
233
  if delete_local:
234
  shutil.rmtree(tmp, ignore_errors=True)
235
+ log_cb(" βœ“ Limpieza local completa")
236
  else:
237
+ final_dir = output_root / folder_name
238
+ final_dir.mkdir(exist_ok=True)
239
+ for f in tmp.iterdir():
240
+ shutil.move(str(f), str(final_dir / f.name))
241
+ shutil.rmtree(tmp, ignore_errors=True)
242
+ log_cb(f" β„Ή Guardado en: {final_dir}")
243
 
244
+ done_cb(True)
245
 
246
  except Exception as e:
247
  import traceback
 
251
  class App(tk.Tk):
252
  def __init__(self):
253
  super().__init__()
254
+ self.title("Video Converter Pro - L33T & TMDb")
255
+ self.geometry("1150x850")
256
  self.configure(bg=BG)
257
  self.resizable(True, True)
258
+
259
+ self._queue = []
260
+ self._total_queue = 0
261
+ self._current_idx = 0
262
+
263
  self._build()
264
 
265
  def _build(self):
 
269
  style.configure("TFrame", background=BG)
270
  style.configure("Card.TFrame", background=CARD_BG)
271
  style.configure("TLabel", background=BG, foreground=FG, font=FONT_MAIN)
 
272
  style.configure("Header.TLabel", background=BG, foreground=FG, font=FONT_TITLE)
273
+ style.configure("Accent.TButton", background=ACCENT, foreground="white", font=FONT_BOLD, padding=10)
 
274
  style.map("Accent.TButton", background=[("active", ACCENT_H)])
 
 
275
  style.configure("Horizontal.TProgressbar", troughcolor=BG, background=ACCENT, thickness=8)
276
 
277
+ main = tk.Frame(self, bg=BG)
278
+ main.pack(fill="both", expand=True, padx=20, pady=20)
279
 
280
+ header = tk.Frame(main, bg=BG)
281
+ header.pack(fill="x", pady=(0, 20))
282
+ tk.Label(header, text="VIDEO CONVERTER PRO", bg=BG, fg=ACCENT, font=FONT_TITLE).pack(side="left")
283
+ self._lbl_status = tk.Label(header, text="● Listo", bg=BG, fg=GREEN, font=FONT_MAIN)
284
  self._lbl_status.pack(side="right")
285
 
286
+ body = tk.Frame(main, bg=BG)
287
+ body.pack(fill="both", expand=True)
288
+ body.columnconfigure(0, weight=1, uniform="g1")
289
+ body.columnconfigure(1, weight=1, uniform="g1")
290
+ body.rowconfigure(0, weight=1)
 
 
 
291
 
292
+ left = tk.Frame(body, bg=CARD_BG, padx=15, pady=15)
293
+ left.grid(row=0, column=0, sticky="nsew", padx=(0, 10))
294
+ self._build_hf_section(left)
295
+ self._build_tmdb_section(left)
296
+ self._build_options_section(left)
297
 
298
+ right = tk.Frame(body, bg=CARD_BG, padx=15, pady=15)
299
+ right.grid(row=0, column=1, sticky="nsew", padx=(10, 0))
300
+ self._build_source_section(right)
301
+ self._build_tracks_section(right)
302
+ self._build_log_section(right)
303
 
304
  def _create_section(self, parent, title):
305
  f = tk.Frame(parent, bg=CARD_BG, pady=5)
 
313
  s = self._create_section(parent, "HuggingFace")
314
  tk.Label(s, text="Token:", bg=CARD_BG, fg=FG2).pack(anchor="w")
315
  self._hf_token = tk.StringVar()
316
+ tk.Entry(s, textvariable=self._hf_token, show="*", bg=BG, fg=FG, insertbackground=FG, relief="flat").pack(fill="x", ipady=4, pady=(0, 10))
317
 
318
+ tk.Label(s, text="Repositorio:", bg=CARD_BG, fg=FG2).pack(anchor="w")
319
+ cb_f = tk.Frame(s, bg=CARD_BG)
320
+ cb_f.pack(fill="x")
321
+ self._repo_cb = ttk.Combobox(cb_f, state="normal", font=FONT_MAIN)
322
+ self._repo_cb.pack(side="left", fill="x", expand=True)
323
+ tk.Button(cb_f, text="↻", bg=BG, fg=ACCENT, bd=0, font=FONT_BOLD, cursor="hand2", command=self._load_repos).pack(side="right", padx=(5, 0))
324
+
325
+ def _build_tmdb_section(self, parent):
326
+ s = self._create_section(parent, "Metadatos (TMDb)")
327
+ self._use_tmdb = tk.BooleanVar(value=False)
328
+ ttk.Checkbutton(s, text="Generar nombres usando TMDb", variable=self._use_tmdb).pack(anchor="w", pady=(0,5))
329
 
330
+ tk.Label(s, text="API Key TMDb:", bg=CARD_BG, fg=FG2).pack(anchor="w")
331
+ self._tmdb_key = tk.StringVar()
332
+ tk.Entry(s, textvariable=self._tmdb_key, bg=BG, fg=FG, insertbackground=FG, relief="flat", show="*").pack(fill="x", ipady=3, pady=(0,5))
333
+
334
+ f_search = tk.Frame(s, bg=CARD_BG)
335
+ f_search.pack(fill="x", pady=(0,5))
336
+ self._tmdb_type = tk.StringVar(value="serie")
337
+ ttk.Radiobutton(f_search, text="Serie", variable=self._tmdb_type, value="serie", command=self._tmdb_clear).pack(side="left")
338
+ ttk.Radiobutton(f_search, text="PelΓ­cula", variable=self._tmdb_type, value="pelicula", command=self._tmdb_clear).pack(side="left", padx=(5,10))
339
 
340
+ self._tmdb_query = tk.StringVar()
341
+ tk.Entry(f_search, textvariable=self._tmdb_query, bg=BG, fg=FG, insertbackground=FG, relief="flat").pack(side="left", fill="x", expand=True, ipady=3)
342
+ tk.Button(f_search, text="πŸ”", bg=BG, fg=ACCENT, bd=0, command=self._search_tmdb).pack(side="right", padx=(5,0))
343
+
344
+ self._tmdb_res_cb = ttk.Combobox(s, state="readonly", font=FONT_MAIN)
345
+ self._tmdb_res_cb.pack(fill="x", pady=(0,5))
346
+ self._tmdb_res_cb.bind("<<ComboboxSelected>>", self._on_tmdb_res_select)
347
+
348
+ f_ep = tk.Frame(s, bg=CARD_BG)
349
+ f_ep.pack(fill="x")
350
+ self._tmdb_season_cb = ttk.Combobox(f_ep, state="disabled", font=FONT_MAIN, width=13)
351
+ self._tmdb_season_cb.pack(side="left", padx=(0,5))
352
+ self._tmdb_season_cb.bind("<<ComboboxSelected>>", self._on_tmdb_season_select)
353
 
354
+ self._tmdb_ep_cb = ttk.Combobox(f_ep, state="disabled", font=FONT_MAIN)
355
+ self._tmdb_ep_cb.pack(side="left", fill="x", expand=True)
356
+
357
+ self._tmdb_id_map = {}
358
+ self._tmdb_episodes = []
359
+
360
+ def _tmdb_clear(self, *args):
361
+ self._tmdb_res_cb.set("")
362
+ self._tmdb_res_cb.configure(values=[])
363
+ self._tmdb_season_cb.set("")
364
+ self._tmdb_season_cb.configure(state="disabled")
365
+ self._tmdb_ep_cb.set("")
366
+ self._tmdb_ep_cb.configure(state="disabled")
367
+
368
+ def _search_tmdb(self):
369
+ k = self._tmdb_key.get().strip()
370
+ q = self._tmdb_query.get().strip()
371
+ if not k or not q: return
372
+ t = "movie" if self._tmdb_type.get() == "pelicula" else "tv"
373
+ url = f"https://api.themoviedb.org/3/search/{t}?api_key={k}&query={urllib.parse.quote(q)}&language=es-MX"
374
 
375
+ def _go():
376
+ try:
377
+ data = fetch_tmdb(url)
378
+ res, self._tmdb_id_map = [], {}
379
+ for r in data.get('results', [])[:15]:
380
+ title = r.get('title') or r.get('name')
381
+ dt = r.get('release_date') or r.get('first_air_date') or ""
382
+ yr = dt.split('-')[0] if dt else "N/A"
383
+ lbl = f"{title} ({yr})"
384
+ res.append(lbl)
385
+ self._tmdb_id_map[lbl] = r.get('id')
386
+ self.after(0, lambda: self._tmdb_res_cb.configure(values=res))
387
+ if res:
388
+ self.after(0, lambda: self._tmdb_res_cb.set(res[0]))
389
+ self.after(0, self._on_tmdb_res_select)
390
+ except Exception as e:
391
+ self.after(0, lambda: self._log(f"βœ— TMDb Error: {e}"))
392
+ threading.Thread(target=_go, daemon=True).start()
393
+
394
+ def _on_tmdb_res_select(self, *args):
395
+ if self._tmdb_type.get() == "pelicula":
396
+ self._tmdb_season_cb.configure(state="disabled")
397
+ self._tmdb_ep_cb.configure(state="disabled")
398
+ return
399
+
400
+ self._tmdb_season_cb.configure(state="readonly")
401
+ lbl = self._tmdb_res_cb.get()
402
+ tid = self._tmdb_id_map.get(lbl)
403
+ if not tid: return
404
+
405
+ url = f"https://api.themoviedb.org/3/tv/{tid}?api_key={self._tmdb_key.get().strip()}&language=es-MX"
406
+ def _go():
407
+ try:
408
+ data = fetch_tmdb(url)
409
+ seasons = [f"Temporada {s['season_number']}" for s in data.get('seasons', []) if s['season_number'] > 0]
410
+ self.after(0, lambda: self._tmdb_season_cb.configure(values=seasons))
411
+ if seasons:
412
+ self.after(0, lambda: self._tmdb_season_cb.set(seasons[0]))
413
+ self.after(0, self._on_tmdb_season_select)
414
+ except Exception as e:
415
+ self.after(0, lambda: self._log(f"βœ— TMDb Seasons Error: {e}"))
416
+ threading.Thread(target=_go, daemon=True).start()
417
+
418
+ def _on_tmdb_season_select(self, *args):
419
+ self._tmdb_ep_cb.configure(state="readonly")
420
+ tid = self._tmdb_id_map.get(self._tmdb_res_cb.get())
421
+ s_num = self._tmdb_season_cb.get().split(" ")[1]
422
+ url = f"https://api.themoviedb.org/3/tv/{tid}/season/{s_num}?api_key={self._tmdb_key.get().strip()}&language=es-MX"
423
+
424
+ def _go():
425
+ try:
426
+ data = fetch_tmdb(url)
427
+ self._tmdb_episodes = [{'num': e['episode_number'], 'name': e['name']} for e in data.get('episodes', [])]
428
+ ep_strs = [f"Ep {e['num']}: {e['name']}" for e in self._tmdb_episodes]
429
+ self.after(0, lambda: self._tmdb_ep_cb.configure(values=ep_strs))
430
+ if ep_strs: self.after(0, lambda: self._tmdb_ep_cb.set(ep_strs[0]))
431
+ except Exception as e:
432
+ self.after(0, lambda: self._log(f"βœ— TMDb Episodes Error: {e}"))
433
+ threading.Thread(target=_go, daemon=True).start()
434
 
435
  def _build_options_section(self, parent):
436
  s = self._create_section(parent, "ConfiguraciΓ³n")
437
+ tk.Label(s, text="Nombre manual (Si no usas TMDb):", bg=CARD_BG, fg=FG2).pack(anchor="w")
 
 
438
  self._manual_name = tk.StringVar()
439
+ tk.Entry(s, textvariable=self._manual_name, bg=BG, fg=FG, insertbackground=FG, relief="flat").pack(fill="x", ipady=4, pady=(0, 10))
 
 
 
440
 
441
  self._mode = tk.StringVar(value="Copy + MP3")
442
+ f_m = tk.Frame(s, bg=CARD_BG)
443
+ f_m.pack(fill="x")
444
  for m in ["Copy + MP3", "Copy + FLAC", "H264 1080p"]:
445
+ ttk.Radiobutton(f_m, text=m, variable=self._mode, value=m).pack(side="left", padx=(0,10))
446
 
447
  self._gen_single = tk.BooleanVar(value=False)
448
  self._ext_sub = tk.BooleanVar(value=False)
449
  self._del_local = tk.BooleanVar(value=True)
450
 
451
+ opts = tk.Frame(s, bg=CARD_BG)
452
+ opts.pack(fill="x", pady=(10, 0))
453
+ ttk.Checkbutton(opts, text="Audio", variable=self._gen_single).pack(side="left")
454
+ ttk.Checkbutton(opts, text="Subs", variable=self._ext_sub).pack(side="left", padx=10)
455
+ ttk.Checkbutton(opts, text="Borrar Locales", variable=self._del_local).pack(side="left")
456
 
457
  def _build_tracks_section(self, parent):
458
+ s = self._create_section(parent, "Pistas a Extraer")
459
+ f = tk.Frame(s, bg=CARD_BG)
460
+ f.pack(fill="x")
461
+ tk.Label(f, text="Aud:", bg=CARD_BG, fg=FG2).pack(side="left")
462
+ self._aud_cb = ttk.Combobox(f, state="readonly", font=FONT_MAIN, width=15)
463
+ self._aud_cb.pack(side="left", fill="x", expand=True, padx=(5,10))
464
+ tk.Label(f, text="Sub:", bg=CARD_BG, fg=FG2).pack(side="left")
465
+ self._sub_cb = ttk.Combobox(f, state="readonly", font=FONT_MAIN, width=15)
466
+ self._sub_cb.pack(side="left", fill="x", expand=True, padx=(5,0))
467
 
468
  def _build_source_section(self, parent):
469
  s = self._create_section(parent, "Fuente")
470
 
471
+ # --- PESTAΓ‘AS URL / ARCHIVO DE REGRESO ---
472
  tabs = tk.Frame(s, bg=CARD_BG)
473
  tabs.pack(fill="x", pady=(0, 10))
474
+ self._src_mode = tk.StringVar(value="url")
475
 
476
+ self._btn_url = tk.Button(tabs, text="URLs", bg=ACCENT, fg="white", bd=0, padx=10, pady=5, font=FONT_MAIN, command=lambda: self._switch_src("url"))
477
+ self._btn_url.pack(side="left")
478
+ self._btn_file = tk.Button(tabs, text="Archivo", bg=BG, fg=FG2, bd=0, padx=10, pady=5, font=FONT_MAIN, command=lambda: self._switch_src("file"))
479
+ self._btn_file.pack(side="left", padx=5)
480
 
481
+ # --- SECCIΓ“N ARCHIVO LOCAL ---
482
  self._file_frame = tk.Frame(s, bg=CARD_BG)
 
483
  self._file_var = tk.StringVar()
484
  tk.Entry(self._file_frame, textvariable=self._file_var, bg=BG, fg=FG, insertbackground=FG, font=FONT_MAIN, relief="flat").pack(side="left", fill="x", expand=True, ipady=4, padx=(0, 5))
485
  tk.Button(self._file_frame, text="Buscar", bg=BG, fg=ACCENT, bd=0, padx=8, pady=4, font=FONT_MAIN, command=self._browse).pack(side="right")
486
 
487
+ # --- SECCIΓ“N MÚLTIPLES URLs ---
488
  self._url_frame = tk.Frame(s, bg=CARD_BG)
489
+ self._url_text = tk.Text(self._url_frame, bg=BG, fg=FG, insertbackground=FG, font=FONT_MAIN, relief="flat", height=6)
490
+ sb = ttk.Scrollbar(self._url_frame, command=self._url_text.yview)
491
+ self._url_text.configure(yscrollcommand=sb.set)
492
+ self._url_text.pack(side="left", fill="both", expand=True, pady=2)
493
+ sb.pack(side="right", fill="y", pady=2)
494
 
495
+ # --- INFO DE ANÁLISIS ---
496
  self._lbl_info = tk.Label(s, text="Sin anΓ‘lisis", bg=BG, fg=FG2, font=FONT_MAIN, anchor="w", padx=10, pady=5)
497
  self._lbl_info.pack(fill="x", pady=(10, 0))
498
+
499
+ # --- BOTONES ANALIZAR Y PROCESAR ---
500
+ btns = tk.Frame(s, bg=CARD_BG)
501
+ btns.pack(fill="x", pady=(10, 0))
502
+ tk.Button(btns, text="ANALIZAR", bg=BG, fg=ACCENT, bd=0, padx=10, pady=8, font=FONT_BOLD, cursor="hand2", command=self._do_analyze).pack(side="left")
503
+ self._btn_proc = tk.Button(btns, text="PROCESAR COLA", bg=ACCENT, fg="white", bd=0, padx=10, pady=8, font=FONT_BOLD, cursor="hand2", command=self._do_process)
504
+ self._btn_proc.pack(side="right", fill="x", expand=True, padx=(10,0))
505
 
506
+ # Inicializar en modo URL
507
+ self._switch_src("url")
 
 
 
508
 
509
  def _build_log_section(self, parent):
510
  s = self._create_section(parent, "Progreso")
511
+ self._lbl_prog = tk.Label(s, text="Esperando...", bg=CARD_BG, fg=FG2, anchor="w")
512
  self._lbl_prog.pack(fill="x")
513
  self._pbar = ttk.Progressbar(s, mode="determinate", style="Horizontal.TProgressbar")
514
  self._pbar.pack(fill="x", pady=(5, 10))
515
 
516
  log_f = tk.Frame(s, bg=BG, padx=1, pady=1)
517
  log_f.pack(fill="both", expand=True)
518
+ self._log_txt = tk.Text(log_f, bg=BG, fg="#0ea5e9", font=("Consolas", 9), bd=0, relief="flat", state="disabled")
519
  sb = ttk.Scrollbar(log_f, command=self._log_txt.yview)
520
  self._log_txt.configure(yscrollcommand=sb.set)
521
  sb.pack(side="right", fill="y")
522
  self._log_txt.pack(fill="both", expand=True)
523
 
524
+ def _switch_src(self, mode):
525
  self._src_mode.set(mode)
526
  if mode == "file":
527
  self._file_frame.pack(fill="x")
528
  self._url_frame.pack_forget()
529
+ self._btn_file.configure(bg=ACCENT, fg="white")
530
+ self._btn_url.configure(bg=BG, fg=FG2)
531
  else:
532
  self._url_frame.pack(fill="x")
533
  self._file_frame.pack_forget()
534
+ self._btn_url.configure(bg=ACCENT, fg="white")
535
+ self._btn_file.configure(bg=BG, fg=FG2)
536
 
537
  def _browse(self):
538
  p = filedialog.askopenfilename(filetypes=[("Video", "*.mp4 *.mkv *.avi *.mov *.ts"), ("Todos", "*.*")])
539
  if p:
540
  self._file_var.set(p)
541
 
542
+ def _get_sources(self):
543
+ if self._src_mode.get() == "file":
544
+ p = self._file_var.get().strip()
545
+ return ([p] if p else []), False
546
+ else:
547
+ srcs = [ln.strip() for ln in self._url_text.get("1.0", "end").split("\n") if ln.strip()]
548
+ return srcs, True
549
+
550
  def _log(self, msg):
551
  self._log_txt.configure(state="normal")
552
  self._log_txt.insert("end", msg + "\n")
 
558
  self._lbl_prog.configure(text=label)
559
  self.update_idletasks()
560
 
 
 
 
 
 
561
  def _load_repos(self):
562
+ t = self._hf_token.get().strip()
563
+ if not t or not HF_OK: return
 
 
 
 
 
 
 
564
  self._log("⟳ Cargando repositorios...")
 
565
  def _go():
566
  try:
567
  api = HfApi()
568
+ repos = [m.modelId for m in api.list_models(author=api.whoami(token=t).get('name'), token=t, limit=100)]
 
 
 
 
 
 
 
 
569
  self.after(0, lambda: self._repo_cb.configure(values=repos))
570
+ if repos: self.after(0, lambda: self._repo_cb.set(repos[0]))
571
+ self._log(f"βœ“ {len(repos)} repositorios listos.")
572
+ except Exception as e: self._log(f"βœ— Error HF: {e}")
 
 
 
 
573
  threading.Thread(target=_go, daemon=True).start()
574
 
575
  def _do_analyze(self):
576
+ sources, is_url = self._get_sources()
577
+ if not sources:
578
+ messagebox.showwarning("Error", "Selecciona un archivo o ingresa al menos una URL.")
579
  return
580
+
581
  def _go():
582
  self._log("⟳ Analizando...")
583
+
584
+ # Analizar solo el primer elemento para extraer pistas
585
+ source = sources[0]
586
  info = probe(source)
 
587
  ac = [f"[{t['idx']}] {t['lang']} Β· {t['codec']}" for t in info["audio"]]
588
  sc = [f"[{t['idx']}] {t['lang']} Β· {t['codec']}" for t in info["subs"]]
589
 
 
592
  if ac: self.after(0, lambda: self._aud_cb.set(ac[0]))
593
  if sc: self.after(0, lambda: self._sub_cb.set(sc[0]))
594
 
595
+ if len(sources) > 1:
596
+ txt = f"βœ“ {len(sources)} ELEMENTOS DETECTADOS | Modo Bulk Activado"
597
+ else:
598
+ txt = f"βœ“ {info['title'] or 'Video'} | {fmt_dur(info['duration'])} | {len(ac)} aud | {len(sc)} sub"
599
+
600
  self.after(0, lambda: self._lbl_info.configure(text=txt, fg=GREEN))
601
  self._log(txt)
602
+ if len(sources) > 1:
603
+ self._log("β„Ή Nota: En modo Bulk, las pistas seleccionadas aplicarΓ‘n a todos los videos de la lista.")
604
+
605
  threading.Thread(target=_go, daemon=True).start()
606
 
607
  def _do_process(self):
608
+ tok = self._hf_token.get().strip()
609
+ rep = self._repo_cb.get().strip()
610
+ sources, is_url = self._get_sources()
611
 
612
+ if not tok or not rep or not sources:
613
+ return messagebox.showwarning("Error", "Faltan datos (Token, Repo o Fuente).")
 
 
614
 
615
+ self._queue = sources.copy()
616
+ self._total_queue = len(self._queue)
617
+ self._current_idx = 0
 
 
 
 
 
 
 
 
 
618
 
619
  self._btn_proc.configure(state="disabled", bg=FG2)
 
620
  self._log_txt.configure(state="normal")
621
  self._log_txt.delete("1.0", "end")
622
  self._log_txt.configure(state="disabled")
623
+
624
+ self._process_next()
625
 
626
+ def _process_next(self):
627
+ if not self._queue:
628
  self._btn_proc.configure(state="normal", bg=ACCENT)
629
+ self._lbl_status.configure(text="● Completado", fg=GREEN)
630
+ self._set_progress(100, "Todos completados")
631
+ return self._log("\nβœ“βœ“ COLA FINALIZADA βœ“βœ“")
632
+
633
+ src = self._queue.pop(0)
634
+ self._current_idx += 1
635
+ self._lbl_status.configure(text=f"● Procesando ({self._current_idx}/{self._total_queue})…", fg=YELLOW)
636
+ self._log(f"\nβ–Ά [{self._current_idx}/{self._total_queue}] Inciando...")
637
+
638
+ # ─── LΓ“GICA DE NOMBRES CON TMDb y LEET SPEAK ───
639
+ folder_name, file_name = "Bulk_Upload", f"Video_{self._current_idx}"
640
+
641
+ if self._use_tmdb.get() and self._tmdb_res_cb.get():
642
+ base_lbl = self._tmdb_res_cb.get().split(" (")[0]
643
+ if self._tmdb_type.get() == "pelicula":
644
+ folder_name = to_leet(base_lbl)
645
+ file_name = to_leet(f"{base_lbl} Parte {self._current_idx}") if self._total_queue > 1 else to_leet(base_lbl)
646
+ else:
647
+ s_num = self._tmdb_season_cb.get().split(" ")[1] if self._tmdb_season_cb.get() else "1"
648
+ folder_name = to_leet(f"{base_lbl} S{int(s_num):02d}")
649
+
650
+ ep_idx = self._tmdb_ep_cb.current() if self._tmdb_ep_cb.current() >= 0 else 0
651
+ tgt = ep_idx + self._current_idx - 1
652
+
653
+ if tgt < len(self._tmdb_episodes):
654
+ ep = self._tmdb_episodes[tgt]
655
+ file_name = to_leet(f"{base_lbl} S{int(s_num):02d}E{ep['num']:02d} {ep['name']}")
656
+ else:
657
+ file_name = to_leet(f"{base_lbl} S{int(s_num):02d}E{(tgt+1):02d}")
658
+ else:
659
+ m_name = self._manual_name.get().strip()
660
+ if m_name:
661
+ folder_name = to_leet(m_name)
662
+ file_name = to_leet(f"{m_name} {self._current_idx}") if self._total_queue > 1 else to_leet(m_name)
663
  else:
664
+ # Si no hay manual name ni TMDb y es local file, sacarlo del nombre original
665
+ _, is_url_now = self._get_sources()
666
+ if not is_url_now:
667
+ base = Path(src).stem
668
+ folder_name = to_leet(base)
669
+ file_name = to_leet(base)
670
+
671
+ ai, si = 0, 0
672
+ try:
673
+ if self._aud_cb.get(): ai = int(self._aud_cb.get().split("]")[0].strip("["))
674
+ if self._sub_cb.get(): si = int(self._sub_cb.get().split("]")[0].strip("["))
675
+ except: pass
676
+
677
+ _, is_url_check = self._get_sources()
678
 
679
  threading.Thread(
680
  target=process_video,
681
+ args=(self._hf_token.get().strip(), self._repo_cb.get().strip(), src, is_url_check,
682
+ self._mode.get(), ai, self._gen_single.get(), self._ext_sub.get(), si,
683
+ self._del_local.get(), folder_name, file_name,
684
  lambda m: self.after(0, self._log, m),
685
  lambda p, l: self.after(0, self._set_progress, p, l),
686
+ lambda ok: self.after(0, self._process_next)),
687
  daemon=True
688
  ).start()
689