Update hug.py
Browse files
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
|
| 41 |
HF_OK = True
|
| 42 |
except ImportError:
|
| 43 |
-
pass
|
| 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,
|
| 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 |
-
|
| 207 |
-
tmp
|
|
|
|
| 208 |
|
| 209 |
-
out_mp4 = tmp / f"{
|
| 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"{
|
| 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
|
| 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"{
|
| 251 |
sc = ["ffmpeg", "-y"] + extra + [
|
| 252 |
"-i", source, "-map", f"0:s:{sub_idx}", "-c:s", "webvtt", str(vtt)]
|
| 253 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
|
| 262 |
if delete_local:
|
| 263 |
shutil.rmtree(tmp, ignore_errors=True)
|
| 264 |
-
log_cb(" β
|
| 265 |
else:
|
| 266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
|
| 268 |
-
done_cb(
|
| 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("
|
| 280 |
self.configure(bg=BG)
|
| 281 |
self.resizable(True, True)
|
| 282 |
-
|
| 283 |
-
self.
|
|
|
|
|
|
|
|
|
|
| 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("
|
| 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 |
-
|
| 303 |
-
|
| 304 |
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
tk.Label(
|
| 308 |
-
self._lbl_status = tk.Label(
|
| 309 |
self._lbl_status.pack(side="right")
|
| 310 |
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 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 |
-
|
| 321 |
-
|
| 322 |
-
self.
|
|
|
|
|
|
|
| 323 |
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
self.
|
| 328 |
-
self._build_log_section(
|
| 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,
|
| 343 |
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
|
| 356 |
-
self.
|
| 357 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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(
|
| 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 |
-
|
| 399 |
-
|
| 400 |
-
ttk.Checkbutton(
|
| 401 |
-
ttk.Checkbutton(
|
| 402 |
-
ttk.Checkbutton(
|
| 403 |
|
| 404 |
def _build_tracks_section(self, parent):
|
| 405 |
-
s = self._create_section(parent, "Pistas
|
| 406 |
-
tk.
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
self.
|
| 411 |
-
|
|
|
|
|
|
|
| 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="
|
| 419 |
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 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.
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
self.
|
|
|
|
| 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 |
-
|
| 441 |
-
|
| 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,
|
| 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"
|
| 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
|
| 462 |
self._src_mode.set(mode)
|
| 463 |
if mode == "file":
|
| 464 |
self._file_frame.pack(fill="x")
|
| 465 |
self._url_frame.pack_forget()
|
| 466 |
-
|
| 467 |
-
|
| 468 |
else:
|
| 469 |
self._url_frame.pack(fill="x")
|
| 470 |
self._file_frame.pack_forget()
|
| 471 |
-
|
| 472 |
-
|
| 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 |
-
|
| 497 |
-
if not
|
| 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 |
-
|
| 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 |
-
|
| 522 |
-
|
| 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 |
-
|
| 531 |
-
if not
|
| 532 |
-
messagebox.showwarning("Error", "Selecciona una
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
source, is_url = self._get_source()
|
| 559 |
-
manual_name = self._manual_name.get().strip()
|
| 560 |
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 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 |
-
|
|
|
|
| 580 |
|
| 581 |
-
|
|
|
|
| 582 |
self._btn_proc.configure(state="normal", bg=ACCENT)
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 587 |
else:
|
| 588 |
-
|
| 589 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 590 |
|
| 591 |
threading.Thread(
|
| 592 |
target=process_video,
|
| 593 |
-
args=(
|
| 594 |
-
ai, self._gen_single.get(), self._ext_sub.get(), si,
|
| 595 |
-
self._del_local.get(),
|
| 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,
|
| 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 |
|