import os import ctypes import subprocess import threading import shutil import re import json import uuid import urllib.request import urllib.parse import tkinter as tk from tkinter import ttk, filedialog, messagebox from pathlib import Path # ─── IMPORTS OPCIONALES / DEFERIDOS ───────────────────────── def get_sys(): import sys return sys # ─── AUTO-ELEVACIÓN ADMINISTRADOR ─────────────────────────── def is_admin(): try: return ctypes.windll.shell32.IsUserAnAdmin() except: return False def elevate(): if not is_admin(): sys = get_sys() ctypes.windll.shell32.ShellExecuteW( None, "runas", sys.executable, " ".join(sys.argv), None, 1 ) sys.exit() # ─── VERIFICACIÓN DE LIBRERÍA HF ──────────────────────────── HF_OK = False try: from huggingface_hub import HfApi HF_OK = True except ImportError: pass # ─── ESTILOS Y COLORES ────────────────────────────────────── BG = "#0f172a" CARD_BG = "#1e293b" ACCENT = "#3b82f6" ACCENT_H = "#2563eb" GREEN = "#22c55e" RED = "#ef4444" YELLOW = "#f59e0b" FG = "#f1f5f9" FG2 = "#94a3b8" BORDER = "#334155" FONT_MAIN = ("Segoe UI", 10) FONT_BOLD = ("Segoe UI", 10, "bold") FONT_TITLE= ("Segoe UI", 14, "bold") # ─── FUNCIONES UTILITARIAS ────────────────────────────────── def safe_name(s, maxlen=120): if not s: return "video" return re.sub(r'[<>:"/\\|?*\x00-\x1f]', '_', s).strip()[:maxlen] def to_leet(text): if not text: return "video" rep = { 'a': '4', 'A': '4', 'i': '1', 'I': '1', 't': '7', 'T': '7', 'o': '0', 'O': '0', 'e': '3', 'E': '3' } res = "".join(rep.get(c, c) for c in text) return safe_name(res) def fmt_dur(secs): s = int(secs) return f"{s//3600:02d}:{(s%3600)//60:02d}:{s%60:02d}" def fetch_tmdb(url): req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) with urllib.request.urlopen(req) as resp: return json.loads(resp.read().decode()) def probe(source): sys = get_sys() info = {"audio": [], "subs": [], "duration": 0.0, "title": ""} try: r = subprocess.run( ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", source], capture_output=True, text=True, timeout=90 ) if r.returncode != 0: return info data = json.loads(r.stdout) tags = data.get("format", {}).get("tags", {}) info["title"] = (tags.get("title") or tags.get("TITLE") or "").strip() info["duration"] = float(data.get("format", {}).get("duration", 0) or 0) ai = si = 0 for s in data.get("streams", []): t = s.get("tags", {}) ct = s.get("codec_type", "") if ct == "audio": info["audio"].append({ "idx": ai, "codec": s.get("codec_name", "?"), "lang": t.get("language", "und"), "ch": s.get("channels", 2), "title": t.get("title", ""), }) ai += 1 elif ct == "subtitle": info["subs"].append({ "idx": si, "codec": s.get("codec_name", "?"), "lang": t.get("language", "und"), "title": t.get("title", ""), "forced": s.get("disposition", {}).get("forced", 0) == 1, }) si += 1 except Exception: pass return info NET_ARGS = [ "-user_agent", "Mozilla/5.0", "-headers", "Referer: https://google.com\r\n", "-timeout", "180000000", "-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_at_eof", "1", "-reconnect_delay_max", "30", "-rw_timeout", "180000000", "-multiple_requests", "1", ] def run_ffmpeg(cmd, total, log_cb, progress_cb, label): try: proc = subprocess.Popen( cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, universal_newlines=True, bufsize=1 ) pat = re.compile(r"time=(\d+):(\d+):(\d+)\.(\d+)") for line in proc.stderr: m = pat.search(line) if m: h, mi, s, cs = map(int, m.groups()) cur = h*3600 + mi*60 + s + cs/100 pct = min(99, int(cur / max(total, 1) * 100)) progress_cb(pct, f"{label}: {pct}% [{fmt_dur(cur)} / {fmt_dur(total)}]") proc.wait() return proc.returncode == 0 except Exception as e: log_cb(f" ✗ ffmpeg: {e}") return False def process_video(token, repo_id, source, is_url, mode, audio_idx, gen_single, extract_sub, sub_idx, delete_local, folder_name, file_name, log_cb, progress_cb, done_cb): try: extra = NET_ARGS if is_url else [] log_cb(f"⟳ Analizando fuente: {source[:50]}...") info = probe(source) dur = info["duration"] if info["duration"] > 0 else 1 sys = get_sys() script_dir = Path(sys.argv[0]).parent.resolve() output_root = script_dir / "Videos_Procesados" output_root.mkdir(exist_ok=True) uid = str(uuid.uuid4())[:8] tmp = output_root / f"temp_{uid}" tmp.mkdir(exist_ok=True, parents=True) out_mp4 = tmp / f"{file_name}.mp4" cmd = ["ffmpeg", "-y"] + extra + ["-i", source, "-map", "0:v:0"] for i in range(len(info["audio"])): cmd.extend(["-map", f"0:a:{i}"]) if mode == "Copy + MP3": cmd += ["-c:v", "copy"] for i in range(len(info["audio"])): cmd += [f"-c:a:{i}", "libmp3lame", f"-b:a:{i}", "320k"] elif mode == "Copy + FLAC": cmd += ["-c:v", "copy"] for i in range(len(info["audio"])): cmd += [f"-c:a:{i}", "flac"] else: cmd += ["-c:v", "libx264", "-vf", "scale=-2:1080", "-preset", "fast", "-crf", "18"] for i in range(len(info["audio"])): cmd += [f"-c:a:{i}", "libmp3lame", f"-b:a:{i}", "320k"] cmd += ["-map_metadata", "0", str(out_mp4)] log_cb(f"⚙ Convirtiendo ({mode})…") ok = run_ffmpeg(cmd, dur, log_cb, progress_cb, "Convirtiendo") if not ok: log_cb("✗ Conversión falló") if delete_local: shutil.rmtree(tmp, ignore_errors=True) done_cb(False) return progress_cb(100, "Conversión OK") if gen_single and info["audio"] and audio_idx < len(info["audio"]): sp = tmp / f"{file_name}_aud{audio_idx}.mp4" sc = ["ffmpeg", "-y", "-i", str(out_mp4), "-map", "0:v:0", "-map", f"0:a:{audio_idx}", "-c", "copy", str(sp)] run_ffmpeg(sc, dur, log_cb, progress_cb, "Audio extraído") if extract_sub and info["subs"] and sub_idx < len(info["subs"]): vtt = tmp / f"{file_name}_sub{sub_idx}.vtt" sc = ["ffmpeg", "-y"] + extra + [ "-i", source, "-map", f"0:s:{sub_idx}", "-c:s", "webvtt", str(vtt)] subprocess.run(sc, capture_output=True, check=False) log_cb(f"☁ Subiendo a HuggingFace → {repo_id}") if HF_OK: api = HfApi() files = list(tmp.iterdir()) for i, f in enumerate(files): if f.is_file(): rpath = f"videos/{folder_name}/{f.name}" log_cb(f" ↑ {f.name} -> {rpath}") progress_cb(int((i/len(files))*100), f"Subiendo {i+1}/{len(files)}") try: api.upload_file( path_or_fileobj=str(f), path_in_repo=rpath, repo_id=repo_id, repo_type="model", token=token ) except Exception as e: log_cb(f" ✗ Error subiendo {f.name}: {e}") else: log_cb(" ✗ HuggingFace no disponible.") if delete_local: shutil.rmtree(tmp, ignore_errors=True) log_cb(" ✓ Limpieza local completa") else: final_dir = output_root / folder_name final_dir.mkdir(exist_ok=True) for f in tmp.iterdir(): shutil.move(str(f), str(final_dir / f.name)) shutil.rmtree(tmp, ignore_errors=True) log_cb(f" ℹ Guardado en: {final_dir}") done_cb(True) except Exception as e: import traceback log_cb(f"✗ Error: {e}\n{traceback.format_exc()}") done_cb(False) class App(tk.Tk): def __init__(self): super().__init__() self.title("Video Converter Pro - L33T & TMDb") self.geometry("1150x850") self.configure(bg=BG) self.resizable(True, True) self._queue = [] self._total_queue = 0 self._current_idx = 0 self._build() def _build(self): style = ttk.Style(self) style.theme_use("clam") style.configure(".", background=BG, foreground=FG, font=FONT_MAIN, borderwidth=0) style.configure("TFrame", background=BG) style.configure("Card.TFrame", background=CARD_BG) style.configure("TLabel", background=BG, foreground=FG, font=FONT_MAIN) style.configure("Header.TLabel", background=BG, foreground=FG, font=FONT_TITLE) style.configure("Accent.TButton", background=ACCENT, foreground="white", font=FONT_BOLD, padding=10) style.map("Accent.TButton", background=[("active", ACCENT_H)]) style.configure("Horizontal.TProgressbar", troughcolor=BG, background=ACCENT, thickness=8) main = tk.Frame(self, bg=BG) main.pack(fill="both", expand=True, padx=20, pady=20) header = tk.Frame(main, bg=BG) header.pack(fill="x", pady=(0, 20)) tk.Label(header, text="VIDEO CONVERTER PRO", bg=BG, fg=ACCENT, font=FONT_TITLE).pack(side="left") self._lbl_status = tk.Label(header, text="● Listo", bg=BG, fg=GREEN, font=FONT_MAIN) self._lbl_status.pack(side="right") body = tk.Frame(main, bg=BG) body.pack(fill="both", expand=True) body.columnconfigure(0, weight=1, uniform="g1") body.columnconfigure(1, weight=1, uniform="g1") body.rowconfigure(0, weight=1) left = tk.Frame(body, bg=CARD_BG, padx=15, pady=15) left.grid(row=0, column=0, sticky="nsew", padx=(0, 10)) self._build_hf_section(left) self._build_tmdb_section(left) self._build_options_section(left) right = tk.Frame(body, bg=CARD_BG, padx=15, pady=15) right.grid(row=0, column=1, sticky="nsew", padx=(10, 0)) self._build_source_section(right) self._build_tracks_section(right) self._build_log_section(right) def _create_section(self, parent, title): f = tk.Frame(parent, bg=CARD_BG, pady=5) f.pack(fill="x", pady=(0, 10)) tk.Label(f, text=title.upper(), bg=CARD_BG, fg=FG2, font=("Segoe UI", 8, "bold")).pack(anchor="w") sep = tk.Frame(f, bg=BORDER, height=1) sep.pack(fill="x", pady=(5, 10)) return f def _build_hf_section(self, parent): s = self._create_section(parent, "HuggingFace") tk.Label(s, text="Token:", bg=CARD_BG, fg=FG2).pack(anchor="w") self._hf_token = tk.StringVar() tk.Entry(s, textvariable=self._hf_token, show="*", bg=BG, fg=FG, insertbackground=FG, relief="flat").pack(fill="x", ipady=4, pady=(0, 10)) tk.Label(s, text="Repositorio:", bg=CARD_BG, fg=FG2).pack(anchor="w") cb_f = tk.Frame(s, bg=CARD_BG) cb_f.pack(fill="x") self._repo_cb = ttk.Combobox(cb_f, state="normal", font=FONT_MAIN) self._repo_cb.pack(side="left", fill="x", expand=True) 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)) def _build_tmdb_section(self, parent): s = self._create_section(parent, "Metadatos (TMDb)") self._use_tmdb = tk.BooleanVar(value=False) ttk.Checkbutton(s, text="Generar nombres usando TMDb", variable=self._use_tmdb).pack(anchor="w", pady=(0,5)) tk.Label(s, text="API Key TMDb:", bg=CARD_BG, fg=FG2).pack(anchor="w") self._tmdb_key = tk.StringVar() tk.Entry(s, textvariable=self._tmdb_key, bg=BG, fg=FG, insertbackground=FG, relief="flat", show="*").pack(fill="x", ipady=3, pady=(0,5)) f_search = tk.Frame(s, bg=CARD_BG) f_search.pack(fill="x", pady=(0,5)) self._tmdb_type = tk.StringVar(value="serie") ttk.Radiobutton(f_search, text="Serie", variable=self._tmdb_type, value="serie", command=self._tmdb_clear).pack(side="left") ttk.Radiobutton(f_search, text="Película", variable=self._tmdb_type, value="pelicula", command=self._tmdb_clear).pack(side="left", padx=(5,10)) self._tmdb_query = tk.StringVar() 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) tk.Button(f_search, text="🔍", bg=BG, fg=ACCENT, bd=0, command=self._search_tmdb).pack(side="right", padx=(5,0)) self._tmdb_res_cb = ttk.Combobox(s, state="readonly", font=FONT_MAIN) self._tmdb_res_cb.pack(fill="x", pady=(0,5)) self._tmdb_res_cb.bind("<>", self._on_tmdb_res_select) f_ep = tk.Frame(s, bg=CARD_BG) f_ep.pack(fill="x") self._tmdb_season_cb = ttk.Combobox(f_ep, state="disabled", font=FONT_MAIN, width=13) self._tmdb_season_cb.pack(side="left", padx=(0,5)) self._tmdb_season_cb.bind("<>", self._on_tmdb_season_select) self._tmdb_ep_cb = ttk.Combobox(f_ep, state="disabled", font=FONT_MAIN) self._tmdb_ep_cb.pack(side="left", fill="x", expand=True) self._tmdb_id_map = {} self._tmdb_episodes = [] def _tmdb_clear(self, *args): self._tmdb_res_cb.set("") self._tmdb_res_cb.configure(values=[]) self._tmdb_season_cb.set("") self._tmdb_season_cb.configure(state="disabled") self._tmdb_ep_cb.set("") self._tmdb_ep_cb.configure(state="disabled") def _search_tmdb(self): k = self._tmdb_key.get().strip() q = self._tmdb_query.get().strip() if not k or not q: return t = "movie" if self._tmdb_type.get() == "pelicula" else "tv" url = f"https://api.themoviedb.org/3/search/{t}?api_key={k}&query={urllib.parse.quote(q)}&language=es-MX" def _go(): try: data = fetch_tmdb(url) res, self._tmdb_id_map = [], {} for r in data.get('results', [])[:15]: title = r.get('title') or r.get('name') dt = r.get('release_date') or r.get('first_air_date') or "" yr = dt.split('-')[0] if dt else "N/A" lbl = f"{title} ({yr})" res.append(lbl) self._tmdb_id_map[lbl] = r.get('id') self.after(0, lambda: self._tmdb_res_cb.configure(values=res)) if res: self.after(0, lambda: self._tmdb_res_cb.set(res[0])) self.after(0, self._on_tmdb_res_select) except Exception as e: self.after(0, lambda: self._log(f"✗ TMDb Error: {e}")) threading.Thread(target=_go, daemon=True).start() def _on_tmdb_res_select(self, *args): if self._tmdb_type.get() == "pelicula": self._tmdb_season_cb.configure(state="disabled") self._tmdb_ep_cb.configure(state="disabled") return self._tmdb_season_cb.configure(state="readonly") lbl = self._tmdb_res_cb.get() tid = self._tmdb_id_map.get(lbl) if not tid: return url = f"https://api.themoviedb.org/3/tv/{tid}?api_key={self._tmdb_key.get().strip()}&language=es-MX" def _go(): try: data = fetch_tmdb(url) seasons = [f"Temporada {s['season_number']}" for s in data.get('seasons', []) if s['season_number'] > 0] self.after(0, lambda: self._tmdb_season_cb.configure(values=seasons)) if seasons: self.after(0, lambda: self._tmdb_season_cb.set(seasons[0])) self.after(0, self._on_tmdb_season_select) except Exception as e: self.after(0, lambda: self._log(f"✗ TMDb Seasons Error: {e}")) threading.Thread(target=_go, daemon=True).start() def _on_tmdb_season_select(self, *args): self._tmdb_ep_cb.configure(state="readonly") tid = self._tmdb_id_map.get(self._tmdb_res_cb.get()) s_num = self._tmdb_season_cb.get().split(" ")[1] url = f"https://api.themoviedb.org/3/tv/{tid}/season/{s_num}?api_key={self._tmdb_key.get().strip()}&language=es-MX" def _go(): try: data = fetch_tmdb(url) self._tmdb_episodes = [{'num': e['episode_number'], 'name': e['name']} for e in data.get('episodes', [])] ep_strs = [f"Ep {e['num']}: {e['name']}" for e in self._tmdb_episodes] self.after(0, lambda: self._tmdb_ep_cb.configure(values=ep_strs)) if ep_strs: self.after(0, lambda: self._tmdb_ep_cb.set(ep_strs[0])) except Exception as e: self.after(0, lambda: self._log(f"✗ TMDb Episodes Error: {e}")) threading.Thread(target=_go, daemon=True).start() def _build_options_section(self, parent): s = self._create_section(parent, "Configuración") tk.Label(s, text="Nombre manual (Si no usas TMDb):", bg=CARD_BG, fg=FG2).pack(anchor="w") self._manual_name = tk.StringVar() tk.Entry(s, textvariable=self._manual_name, bg=BG, fg=FG, insertbackground=FG, relief="flat").pack(fill="x", ipady=4, pady=(0, 10)) self._mode = tk.StringVar(value="Copy + MP3") f_m = tk.Frame(s, bg=CARD_BG) f_m.pack(fill="x") for m in ["Copy + MP3", "Copy + FLAC", "H264 1080p"]: ttk.Radiobutton(f_m, text=m, variable=self._mode, value=m).pack(side="left", padx=(0,10)) self._gen_single = tk.BooleanVar(value=False) self._ext_sub = tk.BooleanVar(value=False) self._del_local = tk.BooleanVar(value=True) opts = tk.Frame(s, bg=CARD_BG) opts.pack(fill="x", pady=(10, 0)) ttk.Checkbutton(opts, text="Audio", variable=self._gen_single).pack(side="left") ttk.Checkbutton(opts, text="Subs", variable=self._ext_sub).pack(side="left", padx=10) ttk.Checkbutton(opts, text="Borrar Locales", variable=self._del_local).pack(side="left") def _build_tracks_section(self, parent): s = self._create_section(parent, "Pistas a Extraer") f = tk.Frame(s, bg=CARD_BG) f.pack(fill="x") tk.Label(f, text="Aud:", bg=CARD_BG, fg=FG2).pack(side="left") self._aud_cb = ttk.Combobox(f, state="readonly", font=FONT_MAIN, width=15) self._aud_cb.pack(side="left", fill="x", expand=True, padx=(5,10)) tk.Label(f, text="Sub:", bg=CARD_BG, fg=FG2).pack(side="left") self._sub_cb = ttk.Combobox(f, state="readonly", font=FONT_MAIN, width=15) self._sub_cb.pack(side="left", fill="x", expand=True, padx=(5,0)) def _build_source_section(self, parent): s = self._create_section(parent, "Fuente") # --- PESTAÑAS URL / ARCHIVO DE REGRESO --- tabs = tk.Frame(s, bg=CARD_BG) tabs.pack(fill="x", pady=(0, 10)) self._src_mode = tk.StringVar(value="url") 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")) self._btn_url.pack(side="left") 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")) self._btn_file.pack(side="left", padx=5) # --- SECCIÓN ARCHIVO LOCAL --- self._file_frame = tk.Frame(s, bg=CARD_BG) self._file_var = tk.StringVar() 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)) 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") # --- SECCIÓN MÚLTIPLES URLs --- self._url_frame = tk.Frame(s, bg=CARD_BG) self._url_text = tk.Text(self._url_frame, bg=BG, fg=FG, insertbackground=FG, font=FONT_MAIN, relief="flat", height=6) sb = ttk.Scrollbar(self._url_frame, command=self._url_text.yview) self._url_text.configure(yscrollcommand=sb.set) self._url_text.pack(side="left", fill="both", expand=True, pady=2) sb.pack(side="right", fill="y", pady=2) # --- INFO DE ANÁLISIS --- self._lbl_info = tk.Label(s, text="Sin análisis", bg=BG, fg=FG2, font=FONT_MAIN, anchor="w", padx=10, pady=5) self._lbl_info.pack(fill="x", pady=(10, 0)) # --- BOTONES ANALIZAR Y PROCESAR --- btns = tk.Frame(s, bg=CARD_BG) btns.pack(fill="x", pady=(10, 0)) 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") 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) self._btn_proc.pack(side="right", fill="x", expand=True, padx=(10,0)) # Inicializar en modo URL self._switch_src("url") def _build_log_section(self, parent): s = self._create_section(parent, "Progreso") self._lbl_prog = tk.Label(s, text="Esperando...", bg=CARD_BG, fg=FG2, anchor="w") self._lbl_prog.pack(fill="x") self._pbar = ttk.Progressbar(s, mode="determinate", style="Horizontal.TProgressbar") self._pbar.pack(fill="x", pady=(5, 10)) log_f = tk.Frame(s, bg=BG, padx=1, pady=1) log_f.pack(fill="both", expand=True) self._log_txt = tk.Text(log_f, bg=BG, fg="#0ea5e9", font=("Consolas", 9), bd=0, relief="flat", state="disabled") sb = ttk.Scrollbar(log_f, command=self._log_txt.yview) self._log_txt.configure(yscrollcommand=sb.set) sb.pack(side="right", fill="y") self._log_txt.pack(fill="both", expand=True) def _switch_src(self, mode): self._src_mode.set(mode) if mode == "file": self._file_frame.pack(fill="x") self._url_frame.pack_forget() self._btn_file.configure(bg=ACCENT, fg="white") self._btn_url.configure(bg=BG, fg=FG2) else: self._url_frame.pack(fill="x") self._file_frame.pack_forget() self._btn_url.configure(bg=ACCENT, fg="white") self._btn_file.configure(bg=BG, fg=FG2) def _browse(self): p = filedialog.askopenfilename(filetypes=[("Video", "*.mp4 *.mkv *.avi *.mov *.ts"), ("Todos", "*.*")]) if p: self._file_var.set(p) def _get_sources(self): if self._src_mode.get() == "file": p = self._file_var.get().strip() return ([p] if p else []), False else: srcs = [ln.strip() for ln in self._url_text.get("1.0", "end").split("\n") if ln.strip()] return srcs, True def _log(self, msg): self._log_txt.configure(state="normal") self._log_txt.insert("end", msg + "\n") self._log_txt.see("end") self._log_txt.configure(state="disabled") def _set_progress(self, pct, label=""): self._pbar["value"] = pct self._lbl_prog.configure(text=label) self.update_idletasks() def _load_repos(self): t = self._hf_token.get().strip() if not t or not HF_OK: return self._log("⟳ Cargando repositorios...") def _go(): try: api = HfApi() repos = [m.modelId for m in api.list_models(author=api.whoami(token=t).get('name'), token=t, limit=100)] self.after(0, lambda: self._repo_cb.configure(values=repos)) if repos: self.after(0, lambda: self._repo_cb.set(repos[0])) self._log(f"✓ {len(repos)} repositorios listos.") except Exception as e: self._log(f"✗ Error HF: {e}") threading.Thread(target=_go, daemon=True).start() def _do_analyze(self): sources, is_url = self._get_sources() if not sources: messagebox.showwarning("Error", "Selecciona un archivo o ingresa al menos una URL.") return def _go(): self._log("⟳ Analizando...") # Analizar solo el primer elemento para extraer pistas source = sources[0] info = probe(source) ac = [f"[{t['idx']}] {t['lang']} · {t['codec']}" for t in info["audio"]] sc = [f"[{t['idx']}] {t['lang']} · {t['codec']}" for t in info["subs"]] self.after(0, lambda: self._aud_cb.configure(values=ac)) self.after(0, lambda: self._sub_cb.configure(values=sc)) if ac: self.after(0, lambda: self._aud_cb.set(ac[0])) if sc: self.after(0, lambda: self._sub_cb.set(sc[0])) if len(sources) > 1: txt = f"✓ {len(sources)} ELEMENTOS DETECTADOS | Modo Bulk Activado" else: txt = f"✓ {info['title'] or 'Video'} | {fmt_dur(info['duration'])} | {len(ac)} aud | {len(sc)} sub" self.after(0, lambda: self._lbl_info.configure(text=txt, fg=GREEN)) self._log(txt) if len(sources) > 1: self._log("ℹ Nota: En modo Bulk, las pistas seleccionadas aplicarán a todos los videos de la lista.") threading.Thread(target=_go, daemon=True).start() def _do_process(self): tok = self._hf_token.get().strip() rep = self._repo_cb.get().strip() sources, is_url = self._get_sources() if not tok or not rep or not sources: return messagebox.showwarning("Error", "Faltan datos (Token, Repo o Fuente).") self._queue = sources.copy() self._total_queue = len(self._queue) self._current_idx = 0 self._btn_proc.configure(state="disabled", bg=FG2) self._log_txt.configure(state="normal") self._log_txt.delete("1.0", "end") self._log_txt.configure(state="disabled") self._process_next() def _process_next(self): if not self._queue: self._btn_proc.configure(state="normal", bg=ACCENT) self._lbl_status.configure(text="● Completado", fg=GREEN) self._set_progress(100, "Todos completados") return self._log("\n✓✓ COLA FINALIZADA ✓✓") src = self._queue.pop(0) self._current_idx += 1 self._lbl_status.configure(text=f"● Procesando ({self._current_idx}/{self._total_queue})…", fg=YELLOW) self._log(f"\n▶ [{self._current_idx}/{self._total_queue}] Inciando...") # ─── LÓGICA DE NOMBRES CON TMDb y LEET SPEAK ─── folder_name, file_name = "Bulk_Upload", f"Video_{self._current_idx}" if self._use_tmdb.get() and self._tmdb_res_cb.get(): base_lbl = self._tmdb_res_cb.get().split(" (")[0] if self._tmdb_type.get() == "pelicula": folder_name = to_leet(base_lbl) file_name = to_leet(f"{base_lbl} Parte {self._current_idx}") if self._total_queue > 1 else to_leet(base_lbl) else: s_num = self._tmdb_season_cb.get().split(" ")[1] if self._tmdb_season_cb.get() else "1" folder_name = to_leet(f"{base_lbl} S{int(s_num):02d}") ep_idx = self._tmdb_ep_cb.current() if self._tmdb_ep_cb.current() >= 0 else 0 tgt = ep_idx + self._current_idx - 1 if tgt < len(self._tmdb_episodes): ep = self._tmdb_episodes[tgt] file_name = to_leet(f"{base_lbl} S{int(s_num):02d}E{ep['num']:02d} {ep['name']}") else: file_name = to_leet(f"{base_lbl} S{int(s_num):02d}E{(tgt+1):02d}") else: m_name = self._manual_name.get().strip() if m_name: folder_name = to_leet(m_name) file_name = to_leet(f"{m_name} {self._current_idx}") if self._total_queue > 1 else to_leet(m_name) else: # Si no hay manual name ni TMDb y es local file, sacarlo del nombre original _, is_url_now = self._get_sources() if not is_url_now: base = Path(src).stem folder_name = to_leet(base) file_name = to_leet(base) ai, si = 0, 0 try: if self._aud_cb.get(): ai = int(self._aud_cb.get().split("]")[0].strip("[")) if self._sub_cb.get(): si = int(self._sub_cb.get().split("]")[0].strip("[")) except: pass _, is_url_check = self._get_sources() threading.Thread( target=process_video, args=(self._hf_token.get().strip(), self._repo_cb.get().strip(), src, is_url_check, self._mode.get(), ai, self._gen_single.get(), self._ext_sub.get(), si, self._del_local.get(), folder_name, file_name, lambda m: self.after(0, self._log, m), lambda p, l: self.after(0, self._set_progress, p, l), lambda ok: self.after(0, self._process_next)), daemon=True ).start() if __name__ == "__main__": app = App() app.mainloop()