| import os |
| import subprocess |
| import threading |
| import shutil |
| import re |
| import json |
| import uuid |
| import time |
| import socket |
| import urllib.request |
| import urllib.parse |
| import urllib.error |
| import tkinter as tk |
| from tkinter import ttk, filedialog, messagebox |
| from pathlib import Path |
|
|
| |
| HF_OK = False |
| try: |
| from huggingface_hub import HfApi |
| HF_OK = True |
| except ImportError: |
| pass |
|
|
| |
| TMDB_API_KEY = "3633a0416ea666f002ec317f40bdcf58" |
|
|
| |
| def get_aria2_path(): |
| import sys |
| script_dir = Path(sys.argv[0]).parent.resolve() |
| for name in ["aria2c.exe", "aria2c"]: |
| local_path = script_dir / name |
| if local_path.exists(): |
| return str(local_path) |
| aria2_folder = script_dir / "aria2" |
| if aria2_folder.exists(): |
| for name in ["aria2c.exe", "aria2c"]: |
| local_path = aria2_folder / name |
| if local_path.exists(): |
| return str(local_path) |
| if shutil.which("aria2c"): |
| return "aria2c" |
| return None |
|
|
| ARIA2_PATH = get_aria2_path() |
| ARIA2_OK = ARIA2_PATH is not None |
|
|
| def find_free_port(): |
| with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: |
| s.bind(('127.0.0.1', 0)) |
| return s.getsockname()[1] |
|
|
| def aria2_rpc_call(port, secret, method, params=None): |
| if params is None: |
| params = [] |
| payload = { |
| "jsonrpc": "2.0", |
| "id": str(uuid.uuid4())[:8], |
| "method": f"aria2.{method}", |
| "params": [f"token:{secret}"] + params |
| } |
| try: |
| data = json.dumps(payload).encode() |
| req = urllib.request.Request( |
| f"http://127.0.0.1:{port}/jsonrpc", |
| data=data, |
| headers={"Content-Type": "application/json"}, |
| method="POST" |
| ) |
| with urllib.request.urlopen(req, timeout=10) as r: |
| return json.loads(r.read().decode()) |
| except Exception as e: |
| return {"error": str(e)} |
|
|
| def get_magnet_info(magnet: str, log_cb, progress_cb, cancel_flag, timeout=120): |
| if not ARIA2_OK: |
| log_cb("[X] aria2c no encontrado") |
| return None |
| import sys |
| script_dir = Path(sys.argv[0]).parent.resolve() |
| tmp_dir = script_dir / "Videos_Procesados" / f"magnet_meta_{uuid.uuid4().hex[:8]}" |
| tmp_dir.mkdir(parents=True, exist_ok=True) |
| port = find_free_port() |
| secret = uuid.uuid4().hex[:12] |
| cmd = [ |
| ARIA2_PATH, |
| f"--rpc-listen-port={port}", |
| f"--rpc-secret={secret}", |
| "--enable-rpc=true", |
| "--rpc-listen-all=false", |
| "--seed-time=0", |
| "--file-allocation=none", |
| "--bt-max-peers=50", |
| "--bt-tracker-connect-timeout=15", |
| "--bt-tracker-timeout=15", |
| "--dir", str(tmp_dir), |
| "--summary-interval=1", |
| "--console-log-level=warn", |
| magnet.strip() |
| ] |
| log_cb("[aria2] Obteniendo metadata del magnet...") |
| log_cb(f"[aria2] Puerto RPC: {port}") |
| proc = None |
| try: |
| creationflags = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0 |
| proc = subprocess.Popen( |
| cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, |
| universal_newlines=True, bufsize=1, creationflags=creationflags |
| ) |
| gid = None |
| start_time = time.time() |
| files_info = None |
| waited = 0 |
| while time.time() - start_time < timeout: |
| if cancel_flag[0]: |
| log_cb("[!] Cancelado") |
| break |
| if proc.poll() is not None: |
| log_cb("[!] aria2 se cerro inesperadamente") |
| break |
| result = aria2_rpc_call(port, secret, "tellActive") |
| if "error" not in result and "result" in result: |
| active = result["result"] |
| if active: |
| gid = active[0].get("gid") |
| if gid: |
| result = aria2_rpc_call(port, secret, "tellStatus", [gid, ["files", "bittorrent", "status"]]) |
| if "error" not in result and "result" in result: |
| status = result["result"] |
| bt = status.get("bittorrent", {}) |
| info_dict = bt.get("info", {}) |
| if info_dict and "name" in info_dict: |
| files = status.get("files", []) |
| files_info = { |
| "name": info_dict.get("name", ""), |
| "files": [ |
| { |
| "index": f.get("index", 0), |
| "path": f.get("path", "").replace(str(tmp_dir) + "/", ""), |
| "length": int(f.get("length", 0)), |
| "selected": f.get("selected", "true") == "true" |
| } |
| for f in files |
| ] |
| } |
| log_cb(f"[OK] Metadata: {info_dict.get('name')}") |
| log_cb(f"[OK] {len(files)} archivo(s) en el torrent") |
| for f in files_info["files"]: |
| log_cb(f" [{f['index']}] {f['path']} ({fmt_size(f['length'])})") |
| break |
| waited += 2 |
| if waited % 10 == 0: |
| log_cb(f"[aria2] Esperando metadata... {waited}s") |
| progress_cb(min(90, int(waited / timeout * 100)), f"Obteniendo metadata... {waited}s") |
| time.sleep(2) |
| if proc.poll() is None: |
| if gid: |
| aria2_rpc_call(port, secret, "forcePause", [gid]) |
| time.sleep(0.3) |
| aria2_rpc_call(port, secret, "remove", [gid]) |
| time.sleep(0.3) |
| aria2_rpc_call(port, secret, "shutdown") |
| time.sleep(0.5) |
| proc.terminate() |
| try: |
| proc.wait(timeout=3) |
| except: |
| proc.kill() |
| shutil.rmtree(tmp_dir, ignore_errors=True) |
| return files_info |
| except Exception as e: |
| log_cb(f"[X] Error obteniendo metadata: {e}") |
| if proc and proc.poll() is None: |
| proc.terminate() |
| shutil.rmtree(tmp_dir, ignore_errors=True) |
| return None |
|
|
| def download_with_aria2(source: str, output_dir: Path, log_cb, progress_cb, cancel_flag, |
| selected_files=None) -> bool: |
| if not ARIA2_OK: |
| log_cb("[X] aria2c no encontrado. Coloca aria2c.exe en la carpeta del script") |
| return False |
| output_dir.mkdir(parents=True, exist_ok=True) |
| cmd = [ |
| ARIA2_PATH, |
| "--seed-time=0", |
| "--file-allocation=none", |
| "--max-connection-per-server=16", |
| "--split=16", |
| "--min-split-size=1M", |
| "--bt-max-peers=100", |
| "--bt-request-peer-speed-limit=0", |
| "--bt-tracker-connect-timeout=10", |
| "--bt-tracker-timeout=10", |
| "--console-log-level=notice", |
| "--summary-interval=5", |
| "-d", str(output_dir), |
| ] |
| if selected_files is not None: |
| indices = ",".join(str(i) for i in selected_files) |
| cmd.append(f"--select-file={indices}") |
| log_cb(f"[aria2] Descargando archivos seleccionados: {indices}") |
| cmd.append(source.strip()) |
| log_cb("[aria2] Iniciando descarga...") |
| log_cb(f"[aria2] Destino: {output_dir}") |
| try: |
| creationflags = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0 |
| proc = subprocess.Popen( |
| cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, |
| universal_newlines=True, bufsize=1, creationflags=creationflags |
| ) |
| progress_pattern = re.compile(r'\((\d+)%\)') |
| speed_pattern = re.compile(r'DL:([^\s\]]+)') |
| last_log = 0 |
| for line in proc.stdout: |
| if cancel_flag[0]: |
| proc.terminate() |
| log_cb("[!] Descarga cancelada") |
| return False |
| line = line.strip() |
| if not line: |
| continue |
| pct_match = progress_pattern.search(line) |
| speed_match = speed_pattern.search(line) |
| if pct_match: |
| pct = int(pct_match.group(1)) |
| speed = speed_match.group(1) if speed_match else "..." |
| progress_cb(pct, f"[aria2] {pct}% DL:{speed}") |
| now = time.time() |
| if now - last_log > 5: |
| log_cb(f"[aria2] {pct}% Velocidad: {speed}") |
| last_log = now |
| if "error" in line.lower() and "tracker" not in line.lower(): |
| log_cb(f"[!] {line[:120]}") |
| proc.wait() |
| if proc.returncode == 0: |
| progress_cb(100, "[aria2] Descarga completa") |
| log_cb("[OK] Descarga completada con aria2") |
| return True |
| else: |
| log_cb(f"[X] aria2 termino con codigo: {proc.returncode}") |
| return False |
| except FileNotFoundError: |
| log_cb(f"[X] aria2c no encontrado en: {ARIA2_PATH}") |
| return False |
| except Exception as e: |
| log_cb(f"[X] Error aria2: {e}") |
| return False |
|
|
| |
| VIDEO_EXTS = {".mp4", ".mkv", ".avi", ".mov", ".ts", ".m2ts", ".wmv", ".flv", ".webm", ".mpg", ".mpeg"} |
|
|
| def find_video_files(base_path: Path): |
| return [f for f in sorted(base_path.rglob("*")) if f.is_file() and f.suffix.lower() in VIDEO_EXTS] |
|
|
| def is_torrent_source(source: str) -> bool: |
| s = source.strip().lower() |
| return s.startswith("magnet:") or s.endswith(".torrent") |
|
|
| |
| BG, BG2, BG3, BORDER = "#0a0a0a", "#111111", "#1a1a1a", "#222222" |
| BLUE, GREEN, RED, YELLOW, ORANGE = "#3b82f6", "#10b981", "#ef4444", "#f59e0b", "#f97316" |
| PURPLE = "#a855f7" |
| FG, FG2, FG3 = "#e2e8f0", "#64748b", "#334155" |
| FONT = ("Consolas", 9) |
| FONT_SM = ("Consolas", 8) |
| FONT_LG = ("Consolas", 11, "bold") |
| FONT_XL = ("Consolas", 16, "bold") |
|
|
| |
| 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'} |
| return safe_name("".join(rep.get(c, c) for c in text)) |
|
|
| def fmt_dur(secs): |
| s = int(secs) |
| return f"{s//3600:02d}:{(s%3600)//60:02d}:{s%60:02d}" |
|
|
| def fmt_size(b): |
| if b < 1024: return f"{b} B" |
| if b < 1024**2: return f"{b/1024:.1f} KB" |
| if b < 1024**3: return f"{b/1024**2:.1f} MB" |
| return f"{b/1024**3:.2f} GB" |
|
|
| def fmt_speed(bps): |
| if bps < 1024: return f"{bps} B/s" |
| if bps < 1024**2: return f"{bps/1024:.1f} KB/s" |
| return f"{bps/1024**2:.1f} MB/s" |
|
|
| def fetch_tmdb(url): |
| req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) |
| with urllib.request.urlopen(req, timeout=10) as r: |
| return json.loads(r.read().decode()) |
|
|
| def probe(source): |
| 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, ct = s.get("tags", {}), 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: |
| creationflags = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0 |
| proc = subprocess.Popen( |
| cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, |
| universal_newlines=True, bufsize=1, creationflags=creationflags |
| ) |
| 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"[ERR] ffmpeg: {e}") |
| return False |
|
|
| |
| def extract_all_subs_as_vtt(src, tmp_dir, base_name, log_cb, progress_cb, dur, |
| net_args=None, is_url=False): |
| """ |
| Extrae TODOS los subtΓtulos del video fuente (no renderizado) como archivos .vtt. |
| Primero intenta extraer directamente como webvtt; si falla, extrae como srt y convierte. |
| Retorna lista de paths de archivos vtt generados. |
| """ |
| info = probe(src) |
| subs = info.get("subs", []) |
| vtt_files = [] |
|
|
| if not subs: |
| log_cb("[sub] No se encontraron subtitulos en el video original") |
| return vtt_files |
|
|
| log_cb(f"[sub] Extrayendo {len(subs)} subtitulo(s) del video original...") |
|
|
| for sub in subs: |
| si = sub["idx"] |
| lang = sub.get("lang", "und") |
| codec = sub.get("codec", "?") |
| sub_title = sub.get("title", "").strip() |
| forced_tag = "_forced" if sub.get("forced") else "" |
| |
| |
| label_parts = [f"sub{si:02d}", lang] |
| if sub_title: |
| label_parts.append(safe_name(sub_title, 30)) |
| label = "_".join(label_parts) + forced_tag |
| |
| vtt_out = tmp_dir / f"{base_name}_{label}.vtt" |
|
|
| log_cb(f"[sub] [{si}] {lang} ({codec}) -> {vtt_out.name}") |
|
|
| |
| cmd_vtt = ["ffmpeg", "-y"] |
| if is_url and net_args: |
| cmd_vtt += net_args |
| cmd_vtt += [ |
| "-i", src, |
| "-map", f"0:s:{si}", |
| "-c:s", "webvtt", |
| str(vtt_out) |
| ] |
| ok = run_ffmpeg(cmd_vtt, dur, log_cb, progress_cb, f"Sub {si} -> VTT") |
|
|
| if not ok: |
| |
| log_cb(f"[sub] Fallback SRT->VTT para sub {si}...") |
| srt_out = tmp_dir / f"{base_name}_{label}.srt" |
| cmd_srt = ["ffmpeg", "-y"] |
| if is_url and net_args: |
| cmd_srt += net_args |
| cmd_srt += [ |
| "-i", src, |
| "-map", f"0:s:{si}", |
| "-c:s", "srt", |
| str(srt_out) |
| ] |
| try: |
| creationflags = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0 |
| r = subprocess.run(cmd_srt, capture_output=True, creationflags=creationflags) |
| if r.returncode == 0 and srt_out.exists(): |
| |
| converted = srt_to_vtt(srt_out, vtt_out, log_cb) |
| srt_out.unlink(missing_ok=True) |
| if converted: |
| ok = True |
| else: |
| log_cb(f"[X] No se pudo convertir SRT->VTT para sub {si}") |
| else: |
| log_cb(f"[X] No se pudo extraer sub {si} como SRT") |
| except Exception as e: |
| log_cb(f"[X] Error extrayendo sub {si}: {e}") |
|
|
| if ok and vtt_out.exists() and vtt_out.stat().st_size > 0: |
| log_cb(f"[OK] Sub {si} extraido: {vtt_out.name} ({fmt_size(vtt_out.stat().st_size)})") |
| vtt_files.append(vtt_out) |
| else: |
| log_cb(f"[X] Sub {si} no generado o vacio") |
|
|
| return vtt_files |
|
|
|
|
| def srt_to_vtt(srt_path: Path, vtt_path: Path, log_cb) -> bool: |
| """Convierte un archivo .srt a .vtt manualmente.""" |
| try: |
| content = srt_path.read_text(encoding="utf-8", errors="replace") |
| |
| |
| lines = content.splitlines() |
| vtt_lines = ["WEBVTT", ""] |
| i = 0 |
| while i < len(lines): |
| line = lines[i].strip() |
| |
| if re.match(r'^\d+$', line): |
| i += 1 |
| continue |
| |
| ts_match = re.match(r'(\d{2}:\d{2}:\d{2}),(\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2}),(\d{3})', line) |
| if ts_match: |
| |
| vtt_ts = f"{ts_match.group(1)}.{ts_match.group(2)} --> {ts_match.group(3)}.{ts_match.group(4)}" |
| vtt_lines.append(vtt_ts) |
| i += 1 |
| |
| while i < len(lines) and lines[i].strip() != "": |
| vtt_lines.append(lines[i].rstrip()) |
| i += 1 |
| vtt_lines.append("") |
| continue |
| i += 1 |
| vtt_path.write_text("\n".join(vtt_lines), encoding="utf-8") |
| return True |
| except Exception as e: |
| log_cb(f"[X] Error SRT->VTT: {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, |
| selected_torrent_files, log_cb, progress_cb, done_cb, cancel_flag=None): |
| if cancel_flag is None: |
| cancel_flag = [False] |
| try: |
| import 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) |
|
|
| actual_sources = [source] |
| is_actual_url = is_url |
|
|
| if is_torrent_source(source): |
| log_cb(f"[P2P] Fuente torrent: {source[:80]}...") |
| if not ARIA2_OK: |
| log_cb("[X] aria2c no encontrado") |
| shutil.rmtree(tmp, ignore_errors=True) |
| done_cb(False) |
| return |
| torrent_out = tmp / "torrent_dl" |
| ok = download_with_aria2( |
| source, torrent_out, log_cb, progress_cb, cancel_flag, |
| selected_files=selected_torrent_files |
| ) |
| if not ok: |
| log_cb("[X] No se descargaron videos.") |
| shutil.rmtree(tmp, ignore_errors=True) |
| done_cb(False) |
| return |
| video_files = find_video_files(torrent_out) |
| if not video_files: |
| log_cb("[X] No se encontraron archivos de video en la descarga.") |
| shutil.rmtree(tmp, ignore_errors=True) |
| done_cb(False) |
| return |
| actual_sources = [str(f) for f in video_files] |
| is_actual_url = False |
| log_cb(f"[OK] {len(actual_sources)} archivo(s) listo(s) para procesar") |
|
|
| for src_idx, src in enumerate(actual_sources): |
| if cancel_flag[0]: |
| log_cb("[X] Cancelado") |
| break |
| suffix = f"_{src_idx+1}" if len(actual_sources) > 1 else "" |
| cur_file_name = f"{file_name}{suffix}" |
| out_mp4 = tmp / f"{cur_file_name}.mp4" |
|
|
| log_cb(f"[->] Analizando: {Path(src).name[:50]}...") |
| info = probe(src) |
| dur = info["duration"] if info["duration"] > 0 else 1 |
| log_cb(f"[info] {info['title'] or 'sin titulo'} | {fmt_dur(dur)} | {len(info['audio'])} audio | {len(info['subs'])} subs") |
|
|
| |
| |
| if mode in ("Todos + InglΓ©s", "Todos + Coreano"): |
| lang_target = "eng" if mode == "Todos + InglΓ©s" else "kor" |
| lang_label = "en" if mode == "Todos + InglΓ©s" else "ko" |
| lang_name = "ingles" if mode == "Todos + InglΓ©s" else "coreano" |
|
|
| log_cb(f"\n[modo] {mode}: generando 2 videos + todos los subs del original") |
|
|
| |
| out_all = tmp / f"{cur_file_name}_all_audio.mp4" |
| cmd_all = ["ffmpeg", "-y"] + (NET_ARGS if is_actual_url else []) + ["-i", src, "-map", "0:v:0"] |
| for i in range(len(info["audio"])): |
| cmd_all.extend(["-map", f"0:a:{i}"]) |
| cmd_all += ["-c:v", "copy"] |
| for i in range(len(info["audio"])): |
| cmd_all.extend([f"-c:a:{i}", "libmp3lame", f"-b:a:{i}", "320k"]) |
| cmd_all += ["-map_metadata", "0", str(out_all)] |
| log_cb(f"[ffmpeg] Video 1: todos los audios ({len(info['audio'])} pistas)...") |
| ok1 = run_ffmpeg(cmd_all, dur, log_cb, progress_cb, "Video ALL") |
| if not ok1: |
| log_cb(f"[X] Fallo video ALL: {Path(src).name}") |
| else: |
| log_cb(f"[OK] Video ALL: {out_all.name}") |
|
|
| |
| |
| target_audio_tracks = [a for a in info["audio"] if lang_target in a["lang"].lower()] |
| if not target_audio_tracks: |
| |
| log_cb(f"[!] No se encontraron pistas de audio '{lang_name}'. Usando primera pista.") |
| target_audio_tracks = info["audio"][:1] if info["audio"] else [] |
|
|
| out_lang = tmp / f"{cur_file_name}_{lang_label}_audio.mp4" |
| if target_audio_tracks: |
| cmd_lang = ["ffmpeg", "-y"] + (NET_ARGS if is_actual_url else []) + ["-i", src, "-map", "0:v:0"] |
| for ta in target_audio_tracks: |
| cmd_lang.extend(["-map", f"0:a:{ta['idx']}"]) |
| cmd_lang += ["-c:v", "copy"] |
| for i in range(len(target_audio_tracks)): |
| cmd_lang.extend([f"-c:a:{i}", "libmp3lame", f"-b:a:{i}", "320k"]) |
| cmd_lang += ["-map_metadata", "0", str(out_lang)] |
| log_cb(f"[ffmpeg] Video 2: audio {lang_name} ({len(target_audio_tracks)} pista(s))...") |
| ok2 = run_ffmpeg(cmd_lang, dur, log_cb, progress_cb, f"Video {lang_label.upper()}") |
| if not ok2: |
| log_cb(f"[X] Fallo video {lang_name}: {Path(src).name}") |
| else: |
| log_cb(f"[OK] Video {lang_name}: {out_lang.name}") |
| else: |
| log_cb(f"[X] No hay audios disponibles para video {lang_name}") |
|
|
| |
| log_cb(f"\n[sub] Extrayendo TODOS los subtitulos del video original...") |
| vtt_files = extract_all_subs_as_vtt( |
| src, tmp, cur_file_name, log_cb, progress_cb, dur, |
| net_args=NET_ARGS if is_actual_url else None, |
| is_url=is_actual_url |
| ) |
| log_cb(f"[OK] {len(vtt_files)} subtitulo(s) extraido(s) como VTT") |
|
|
| |
| |
| continue |
|
|
| |
| cmd = ["ffmpeg", "-y"] + (NET_ARGS if is_actual_url else []) + ["-i", src, "-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.extend([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.extend([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.extend([f"-c:a:{i}", "libmp3lame", f"-b:a:{i}", "320k"]) |
|
|
| cmd += ["-map_metadata", "0", str(out_mp4)] |
| log_cb(f"[ffmpeg] Convirtiendo ({mode})...") |
| log_cb(f"[ffmpeg] Audio en video procesado: {len(info['audio'])} pistas") |
| ok = run_ffmpeg(cmd, dur, log_cb, progress_cb, "Convirtiendo") |
| if not ok: |
| log_cb(f"[X] Fallo la conversion: {Path(src).name}") |
| continue |
| progress_cb(100, "Conversion completa") |
|
|
| |
| if gen_single: |
| log_cb(f"[->] Extrayendo audio indice {audio_idx} del video procesado...") |
| sp = tmp / f"{cur_file_name}_aud{audio_idx}.mp4" |
| ffmpeg_audio = [ |
| "ffmpeg", "-y", |
| "-i", str(out_mp4), |
| "-map", "0:v:0", |
| "-map", f"0:a:{audio_idx}", |
| "-c", "copy", |
| str(sp) |
| ] |
| ok_audio = run_ffmpeg(ffmpeg_audio, dur, log_cb, progress_cb, "Extrayendo audio") |
| if ok_audio: |
| log_cb(f"[OK] Audio {audio_idx} extraido: {sp.name}") |
| else: |
| log_cb(f"[X] Fallo al extraer audio {audio_idx}") |
|
|
| |
| if extract_sub: |
| log_cb(f"[->] Extrayendo subtitulo indice {sub_idx} del video procesado...") |
| vtt = tmp / f"{cur_file_name}_sub{sub_idx}.vtt" |
| info_proc = probe(str(out_mp4)) |
| if info_proc["subs"] and sub_idx < len(info_proc["subs"]): |
| ffmpeg_sub = [ |
| "ffmpeg", "-y", |
| "-i", str(out_mp4), |
| "-map", f"0:s:{sub_idx}", |
| "-c:s", "webvtt", |
| str(vtt) |
| ] |
| ok_sub = run_ffmpeg(ffmpeg_sub, dur, log_cb, progress_cb, "Extrayendo sub") |
| else: |
| log_cb(f"[->] Subs no en procesado, extrayendo del original...") |
| ffmpeg_sub = ["ffmpeg", "-y"] + (NET_ARGS if is_actual_url else []) + [ |
| "-i", src, |
| "-map", f"0:s:{sub_idx}", |
| "-c:s", "webvtt", |
| str(vtt) |
| ] |
| ok_sub = subprocess.run(ffmpeg_sub, capture_output=True, check=False).returncode == 0 |
| |
| ok_sub_val = ok_sub if isinstance(ok_sub, bool) else False |
| if ok_sub_val: |
| log_cb(f"[OK] Subtitulo {sub_idx} extraido: {vtt.name}") |
| else: |
| log_cb(f"[X] Fallo al extraer subtitulo {sub_idx}") |
|
|
| log_cb(f"[HF] Subiendo a HuggingFace -> {repo_id}") |
| if HF_OK: |
| api = HfApi() |
| files = [f for f in tmp.iterdir() if f.is_file()] |
| for i, f in enumerate(files): |
| rpath = f"videos/{folder_name}/{f.name}" |
| log_cb(f" {f.name}") |
| 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" [X] {f.name}: {e}") |
| progress_cb(100, "Subida completa") |
|
|
| if delete_local: |
| shutil.rmtree(tmp, ignore_errors=True) |
| log_cb("[OK] Temporales eliminados") |
| 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"[OK] Guardado en: {final_dir}") |
|
|
| done_cb(True) |
|
|
| except Exception as e: |
| import traceback |
| log_cb(f"[X] {e}\n{traceback.format_exc()}") |
| done_cb(False) |
|
|
| |
| class Spinner(tk.Canvas): |
| def __init__(self, parent, size=16, color=BLUE, **kwargs): |
| super().__init__(parent, width=size, height=size, |
| bg=kwargs.pop("bg", BG2), highlightthickness=0, **kwargs) |
| self._angle = 0 |
| self._running = False |
| self._arc = self.create_arc(2, 2, size-2, size-2, start=0, extent=270, |
| outline=color, width=2, style="arc") |
| def start(self): self._running = True; self._spin() |
| def stop(self): self._running = False |
| def _spin(self): |
| if not self._running: return |
| self._angle = (self._angle + 12) % 360 |
| self.itemconfig(self._arc, start=self._angle) |
| self.after(30, self._spin) |
|
|
| class FlatEntry(tk.Entry): |
| def __init__(self, parent, **kwargs): |
| kwargs.setdefault("bg", BG3) |
| kwargs.setdefault("fg", FG) |
| kwargs.setdefault("insertbackground", FG) |
| kwargs.setdefault("relief", "flat") |
| kwargs.setdefault("font", FONT) |
| kwargs.setdefault("highlightthickness", 1) |
| kwargs.setdefault("highlightcolor", BLUE) |
| kwargs.setdefault("highlightbackground", BORDER) |
| super().__init__(parent, **kwargs) |
|
|
| class FlatButton(tk.Button): |
| VARIANTS = { |
| "primary": (BLUE, "white", "#2563eb"), |
| "ghost": (BG3, FG2, BORDER), |
| "torrent": (ORANGE, "white", "#ea580c"), |
| "aria2": ("#22c55e","white", "#16a34a"), |
| "english": ("#1d4ed8","white", "#1e40af"), |
| "korean": (PURPLE, "white", "#9333ea"), |
| "default": (BG2, FG2, BG3), |
| } |
| def __init__(self, parent, variant="default", **kwargs): |
| c = self.VARIANTS.get(variant, self.VARIANTS["default"]) |
| kwargs.setdefault("bg", c[0]) |
| kwargs.setdefault("fg", c[1]) |
| kwargs.setdefault("activebackground", c[2]) |
| kwargs.setdefault("relief", "flat") |
| kwargs.setdefault("font", FONT) |
| kwargs.setdefault("cursor", "hand2") |
| kwargs.setdefault("padx", 10) |
| kwargs.setdefault("pady", 6) |
| kwargs.setdefault("bd", 0) |
| super().__init__(parent, **kwargs) |
|
|
| class Section(tk.Frame): |
| def __init__(self, parent, title="", **kwargs): |
| kwargs.setdefault("bg", BG2) |
| super().__init__(parent, **kwargs) |
| if title: |
| hdr = tk.Frame(self, bg=BG2) |
| hdr.pack(fill="x", pady=(0, 8)) |
| tk.Label(hdr, text=title.upper(), bg=BG2, fg=FG3, font=FONT_SM).pack(side="left") |
| tk.Frame(hdr, bg=BORDER, height=1).pack(side="left", fill="x", expand=True, padx=(8, 0)) |
|
|
| |
| class TorrentFilesDialog(tk.Toplevel): |
| def __init__(self, parent, torrent_info: dict): |
| super().__init__(parent) |
| self.title(f"Archivos: {torrent_info.get('name', 'Torrent')[:50]}") |
| self.geometry("600x450") |
| self.configure(bg=BG) |
| self.transient(parent) |
| self.grab_set() |
| self.result = None |
| self.files = torrent_info.get('files', []) |
| self.check_vars = [] |
| tk.Label(self, text=torrent_info.get('name', 'Torrent'), bg=BG, fg=FG, |
| font=FONT_LG, wraplength=580).pack(pady=(15, 5), padx=15, anchor="w") |
| tk.Label(self, text=f"{len(self.files)} archivo(s)", bg=BG, fg=FG3, |
| font=FONT_SM).pack(padx=15, anchor="w") |
| btn_frame = tk.Frame(self, bg=BG) |
| btn_frame.pack(fill="x", padx=15, pady=(10, 5)) |
| FlatButton(btn_frame, text="Seleccionar videos", variant="aria2", |
| command=self._select_videos).pack(side="left") |
| FlatButton(btn_frame, text="Todos", variant="ghost", |
| command=self._select_all).pack(side="left", padx=(8, 0)) |
| FlatButton(btn_frame, text="Ninguno", variant="ghost", |
| command=self._deselect_all).pack(side="left", padx=(8, 0)) |
| list_frame = tk.Frame(self, bg=BG3, highlightthickness=1, highlightbackground=BORDER) |
| list_frame.pack(fill="both", expand=True, padx=15, pady=(5, 15)) |
| canvas = tk.Canvas(list_frame, bg=BG3, highlightthickness=0) |
| scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview) |
| scroll_frame = tk.Frame(canvas, bg=BG3) |
| scroll_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) |
| canvas.create_window((0, 0), window=scroll_frame, anchor="nw") |
| canvas.configure(yscrollcommand=scrollbar.set) |
| canvas.pack(side="left", fill="both", expand=True) |
| scrollbar.pack(side="right", fill="y") |
| for i, f in enumerate(self.files): |
| var = tk.BooleanVar(value=f.get('selected', True)) |
| self.check_vars.append(var) |
| is_video = Path(f['path']).suffix.lower() in VIDEO_EXTS |
| fg_color = GREEN if is_video else FG2 |
| row = tk.Frame(scroll_frame, bg=BG3) |
| row.pack(fill="x", padx=5, pady=1) |
| tk.Checkbutton(row, variable=var, bg=BG3, selectcolor=BG2, |
| activebackground=BG3, activeforeground=FG).pack(side="left") |
| icon = "V" if is_video else "F" |
| text = f"[{icon}][{i}] {f['path']} ({fmt_size(f['length'])})" |
| tk.Label(row, text=text, bg=BG3, fg=fg_color, font=FONT_SM, anchor="w").pack(side="left", fill="x", expand=True) |
| bottom = tk.Frame(self, bg=BG) |
| bottom.pack(fill="x", padx=15, pady=(0, 15)) |
| FlatButton(bottom, text="CANCELAR", variant="ghost", command=self._cancel).pack(side="right") |
| FlatButton(bottom, text="ACEPTAR", variant="primary", command=self._accept).pack(side="right", padx=(0, 8)) |
| def _select_videos(self): |
| for i, (var, f) in enumerate(zip(self.check_vars, self.files)): |
| var.set(Path(f['path']).suffix.lower() in VIDEO_EXTS) |
| def _select_all(self): |
| for var in self.check_vars: |
| var.set(True) |
| def _deselect_all(self): |
| for var in self.check_vars: |
| var.set(False) |
| def _accept(self): |
| selected = [i for i, var in enumerate(self.check_vars) if var.get()] |
| self.result = selected if selected else None |
| self.destroy() |
| def _cancel(self): |
| self.result = None |
| self.destroy() |
|
|
| |
| class App(tk.Tk): |
| def __init__(self): |
| super().__init__() |
| self.title("vcpro") |
| self.geometry("1160x900") |
| self.configure(bg=BG) |
| self.resizable(True, True) |
| self._queue = [] |
| self._total_queue = 0 |
| self._current_idx = 0 |
| self._cancel_flag = [False] |
| self._torrent_info = None |
| self._selected_files = None |
| self._build_ui() |
| self._update_hf_status() |
| self._update_aria2_status() |
| self._on_track_mode_change() |
|
|
| def _build_ui(self): |
| s = ttk.Style(self) |
| s.theme_use("clam") |
| s.configure(".", background=BG, foreground=FG, font=FONT, borderwidth=0) |
| s.configure("TFrame", background=BG) |
| s.configure("TLabel", background=BG, foreground=FG) |
| s.configure("TCheckbutton", background=BG2, foreground=FG2, font=FONT) |
| s.map("TCheckbutton", background=[("active", BG2)], foreground=[("active", FG)]) |
| s.configure("TRadiobutton", background=BG2, foreground=FG2, font=FONT) |
| s.map("TRadiobutton", background=[("active", BG2)]) |
| s.configure("TCombobox", fieldbackground=BG3, background=BG3, foreground=FG, |
| arrowcolor=FG2, font=FONT) |
| s.map("TCombobox", fieldbackground=[("readonly", BG3)], background=[("readonly", BG3)]) |
| s.configure("Horizontal.TProgressbar", troughcolor=BG3, background=BLUE, thickness=2, borderwidth=0) |
| s.configure("Aria2.Horizontal.TProgressbar", troughcolor=BG3, background="#22c55e", thickness=2, borderwidth=0) |
| s.configure("Korean.Horizontal.TProgressbar", troughcolor=BG3, background=PURPLE, thickness=2, borderwidth=0) |
|
|
| top = tk.Frame(self, bg=BG, pady=14, padx=20) |
| top.pack(fill="x") |
| tk.Label(top, text="vcpro", bg=BG, fg=FG, font=FONT_XL).pack(side="left") |
| tk.Label(top, text="video converter", bg=BG, fg=FG3, font=FONT).pack(side="left", padx=(10, 0)) |
| self._aria2_dot = tk.Label(top, text="β", bg=BG, fg=FG3, font=FONT) |
| self._aria2_dot.pack(side="right") |
| self._aria2_lbl = tk.Label(top, text="aria2:-", bg=BG, fg=FG3, font=FONT_SM) |
| self._aria2_lbl.pack(side="right", padx=(0, 8)) |
| self._hf_dot = tk.Label(top, text="β", bg=BG, fg=FG3, font=FONT) |
| self._hf_dot.pack(side="right") |
| self._hf_lbl = tk.Label(top, text="hf:-", bg=BG, fg=FG3, font=FONT_SM) |
| self._hf_lbl.pack(side="right", padx=(0, 8)) |
| tk.Frame(self, bg=BORDER, height=1).pack(fill="x") |
|
|
| body = tk.Frame(self, bg=BG) |
| body.pack(fill="both", expand=True, padx=20, pady=16) |
| body.columnconfigure(0, weight=4, uniform="col") |
| body.columnconfigure(1, weight=5, uniform="col") |
| body.rowconfigure(0, weight=1) |
| left = tk.Frame(body, bg=BG) |
| right = tk.Frame(body, bg=BG) |
| left.grid(row=0, column=0, sticky="nsew", padx=(0, 10)) |
| right.grid(row=0, column=1, sticky="nsew") |
| self._build_left(left) |
| self._build_right(right) |
|
|
| def _build_left(self, parent): |
| |
| hf = Section(parent, title="HuggingFace"); hf.pack(fill="x", pady=(0, 14)) |
| tk.Label(hf, text="Token", bg=BG2, fg=FG3, font=FONT_SM).pack(anchor="w") |
| tok_row = tk.Frame(hf, bg=BG2); tok_row.pack(fill="x", pady=(2, 8)) |
| self._hf_token = tk.StringVar() |
| FlatEntry(tok_row, textvariable=self._hf_token, show="*").pack(side="left", fill="x", expand=True, ipady=5) |
| self._btn_connect = FlatButton(tok_row, text="conectar", command=self._connect_hf) |
| self._btn_connect.pack(side="right", padx=(6, 0)) |
| self._spinner_hf = Spinner(tok_row, bg=BG2); self._spinner_hf.pack(side="right", padx=(4, 0)) |
| tk.Label(hf, text="Repositorio", bg=BG2, fg=FG3, font=FONT_SM).pack(anchor="w") |
| repo_row = tk.Frame(hf, bg=BG2); repo_row.pack(fill="x", pady=(2, 0)) |
| self._repo_cb = ttk.Combobox(repo_row, state="normal", font=FONT) |
| self._repo_cb.pack(side="left", fill="x", expand=True) |
| FlatButton(repo_row, text="R", command=self._load_repos).pack(side="right", padx=(6, 0)) |
|
|
| |
| td = Section(parent, title="TMDb Metadatos"); td.pack(fill="x", pady=(0, 14)) |
| self._use_tmdb = tk.BooleanVar(value=False) |
| ttk.Checkbutton(td, text="Usar TMDb para nombres", variable=self._use_tmdb).pack(anchor="w", pady=(0, 6)) |
| tk.Label(td, text=f"API Key: {TMDB_API_KEY[:8]}... (integrada)", bg=BG2, fg=GREEN, font=FONT_SM).pack(anchor="w") |
| search_row = tk.Frame(td, bg=BG2); search_row.pack(fill="x", pady=(4, 6)) |
| self._tmdb_type = tk.StringVar(value="serie") |
| ttk.Radiobutton(search_row, text="Serie", variable=self._tmdb_type, value="serie", |
| command=self._tmdb_clear).pack(side="left") |
| ttk.Radiobutton(search_row, text="Pelicula", variable=self._tmdb_type, value="pelicula", |
| command=self._tmdb_clear).pack(side="left", padx=(6, 10)) |
| self._tmdb_query = tk.StringVar() |
| FlatEntry(search_row, textvariable=self._tmdb_query).pack(side="left", fill="x", expand=True, ipady=3) |
| self._spinner_tmdb = Spinner(search_row, bg=BG2); self._spinner_tmdb.pack(side="right", padx=4) |
| FlatButton(search_row, text="buscar", command=self._search_tmdb).pack(side="right") |
| self._tmdb_res_cb = ttk.Combobox(td, state="readonly", font=FONT) |
| self._tmdb_res_cb.pack(fill="x", pady=(0, 6)) |
| self._tmdb_res_cb.bind("<<ComboboxSelected>>", self._on_tmdb_res_select) |
| ep_row = tk.Frame(td, bg=BG2); ep_row.pack(fill="x") |
| self._tmdb_season_cb = ttk.Combobox(ep_row, state="disabled", font=FONT, width=14) |
| self._tmdb_season_cb.pack(side="left", padx=(0, 6)) |
| self._tmdb_season_cb.bind("<<ComboboxSelected>>", self._on_tmdb_season_select) |
| self._tmdb_ep_cb = ttk.Combobox(ep_row, state="disabled", font=FONT) |
| self._tmdb_ep_cb.pack(side="left", fill="x", expand=True) |
| self._tmdb_id_map = {} |
| self._tmdb_episodes = [] |
|
|
| |
| opt = Section(parent, title="Opciones"); opt.pack(fill="x", pady=(0, 14)) |
| tk.Label(opt, text="Nombre manual", bg=BG2, fg=FG3, font=FONT_SM).pack(anchor="w") |
| self._manual_name = tk.StringVar() |
| FlatEntry(opt, textvariable=self._manual_name).pack(fill="x", ipady=4, pady=(2, 10)) |
| |
| tk.Label(opt, text="Modo de conversion", bg=BG2, fg=FG3, font=FONT_SM).pack(anchor="w") |
| |
| |
| mode_row1 = tk.Frame(opt, bg=BG2); mode_row1.pack(fill="x", pady=(2, 2)) |
| self._mode = tk.StringVar(value="Copy + MP3") |
| for m in ["Copy + MP3", "Copy + FLAC", "H264 1080p"]: |
| ttk.Radiobutton(mode_row1, text=m, variable=self._mode, value=m, |
| command=self._on_mode_change).pack(side="left", padx=(0, 10)) |
| |
| |
| mode_row2 = tk.Frame(opt, bg=BG2); mode_row2.pack(fill="x", pady=(2, 6)) |
| |
| rb_en = ttk.Radiobutton(mode_row2, text="Todos + InglΓ©s", variable=self._mode, |
| value="Todos + InglΓ©s", command=self._on_mode_change) |
| rb_en.pack(side="left", padx=(0, 10)) |
| |
| rb_ko = ttk.Radiobutton(mode_row2, text="Todos + Coreano", variable=self._mode, |
| value="Todos + Coreano", command=self._on_mode_change) |
| rb_ko.pack(side="left", padx=(0, 10)) |
| |
| |
| self._mode_desc_lbl = tk.Label(opt, text="", bg=BG2, fg=FG3, font=FONT_SM, |
| wraplength=320, justify="left", anchor="w") |
| self._mode_desc_lbl.pack(fill="x", pady=(0, 4)) |
|
|
| |
| trk = Section(parent, title="Pistas (Audio/Sub)"); trk.pack(fill="x", pady=(0, 14)) |
| |
| mode_trk_row = tk.Frame(trk, bg=BG2); mode_trk_row.pack(fill="x", pady=(0, 8)) |
| self._track_mode = tk.StringVar(value="auto") |
| ttk.Radiobutton(mode_trk_row, text="AUTOMATICO (segun analizar)", |
| variable=self._track_mode, value="auto", |
| command=self._on_track_mode_change).pack(side="left") |
| ttk.Radiobutton(mode_trk_row, text="MANUAL (yo elijo)", |
| variable=self._track_mode, value="manual", |
| command=self._on_track_mode_change).pack(side="left", padx=(10, 0)) |
| |
| tk.Label(trk, text="Pista 1 = indice 0 | Pista 4 = indice 3 | etc.", |
| bg=BG2, fg=YELLOW, font=FONT_SM).pack(anchor="w", pady=(0, 4)) |
| |
| |
| self._trk_special_note = tk.Label(trk, |
| text="[Todos+InglΓ©s / Todos+Coreano]: pistas ignoradas, se usan todas automaticamente", |
| bg=BG2, fg=PURPLE, font=FONT_SM, wraplength=320, justify="left") |
| self._trk_special_note.pack(anchor="w", pady=(0, 8)) |
|
|
| |
| self._track_auto_frame = tk.Frame(trk, bg=BG2) |
| auto_row = tk.Frame(self._track_auto_frame, bg=BG2); auto_row.pack(fill="x") |
| tk.Label(auto_row, text="Audio", bg=BG2, fg=FG3, font=FONT_SM, width=5, anchor="w").pack(side="left") |
| self._aud_cb = ttk.Combobox(auto_row, state="readonly", font=FONT) |
| self._aud_cb.pack(side="left", fill="x", expand=True, padx=(4, 14)) |
| tk.Label(auto_row, text="Sub", bg=BG2, fg=FG3, font=FONT_SM, width=4, anchor="w").pack(side="left") |
| self._sub_cb = ttk.Combobox(auto_row, state="readonly", font=FONT) |
| self._sub_cb.pack(side="left", fill="x", expand=True, padx=(4, 0)) |
| tk.Label(self._track_auto_frame, text="(usa 'analizar' para llenar estos campos)", |
| bg=BG2, fg=FG3, font=FONT_SM).pack(anchor="w", pady=(4, 0)) |
|
|
| |
| self._track_manual_frame = tk.Frame(trk, bg=BG2) |
| man_aud_row = tk.Frame(self._track_manual_frame, bg=BG2); man_aud_row.pack(fill="x", pady=(0, 4)) |
| tk.Label(man_aud_row, text="Audio indice:", bg=BG2, fg=FG3, font=FONT_SM, anchor="w").pack(side="left") |
| self._manual_audio_idx = tk.StringVar(value="0") |
| FlatEntry(man_aud_row, textvariable=self._manual_audio_idx, width=8).pack(side="left", padx=(8, 4), ipady=3) |
| tk.Label(man_aud_row, text="(0 = pista 1, 3 = pista 4)", bg=BG2, fg=FG3, font=FONT_SM).pack(side="left") |
| |
| man_sub_row = tk.Frame(self._track_manual_frame, bg=BG2); man_sub_row.pack(fill="x", pady=(0, 4)) |
| tk.Label(man_sub_row, text="Subtitulo indice:", bg=BG2, fg=FG3, font=FONT_SM, anchor="w").pack(side="left") |
| self._manual_sub_idx = tk.StringVar(value="0") |
| FlatEntry(man_sub_row, textvariable=self._manual_sub_idx, width=8).pack(side="left", padx=(8, 4), ipady=3) |
| tk.Label(man_sub_row, text="(0 = sub 1, 2 = sub 3)", bg=BG2, fg=FG3, font=FONT_SM).pack(side="left") |
|
|
| |
| self._chk_frame = tk.Frame(trk, bg=BG2) |
| self._chk_frame.pack(fill="x", pady=(8, 0)) |
| self._gen_single = tk.BooleanVar(value=False) |
| self._ext_sub = tk.BooleanVar(value=False) |
| self._del_local = tk.BooleanVar(value=True) |
| ttk.Checkbutton(self._chk_frame, text="Extraer audio (del procesado)", variable=self._gen_single).pack(side="left") |
| ttk.Checkbutton(self._chk_frame, text="Extraer sub", variable=self._ext_sub).pack(side="left", padx=10) |
| ttk.Checkbutton(self._chk_frame, text="Borrar locales", variable=self._del_local).pack(side="left") |
|
|
| |
| aria2_sec = Section(parent, title="aria2 (Torrent)"); aria2_sec.pack(fill="x", pady=(0, 0)) |
| self._aria2_status_lbl = tk.Label(aria2_sec, bg=BG2, fg=FG3, font=FONT_SM, text="verificando...", anchor="w") |
| self._aria2_status_lbl.pack(anchor="w", pady=(0, 6)) |
| btn_row = tk.Frame(aria2_sec, bg=BG2); btn_row.pack(fill="x") |
| FlatButton(btn_row, text="verificar aria2", variant="aria2", command=self._check_aria2).pack(side="left") |
| tk.Label(aria2_sec, text="Coloca aria2c.exe junto al script para descargar torrents", |
| bg=BG2, fg=FG2, font=FONT_SM).pack(anchor="w", pady=(8, 0)) |
|
|
| def _on_mode_change(self, *_): |
| """Actualiza la descripcion y visibilidad de controles segun el modo.""" |
| m = self._mode.get() |
| descs = { |
| "Copy + MP3": "Copia video, convierte todos los audios a MP3 320k.", |
| "Copy + FLAC": "Copia video, convierte todos los audios a FLAC lossless.", |
| "H264 1080p": "Re-encoda video a H264 1080p + todos los audios a MP3 320k.", |
| "Todos + InglΓ©s": "Genera 2 videos: 1) todos los audios MP3, 2) solo audio inglΓ©s MP3.\nExtrae TODOS los subs del original como VTT.", |
| "Todos + Coreano": "Genera 2 videos: 1) todos los audios MP3, 2) solo audio coreano MP3.\nExtrae TODOS los subs del original como VTT.", |
| } |
| self._mode_desc_lbl.configure(text=descs.get(m, "")) |
| |
| |
| if m in ("Todos + InglΓ©s", "Todos + Coreano"): |
| self._chk_frame.pack_forget() |
| self._trk_special_note.configure(fg=PURPLE) |
| else: |
| self._chk_frame.pack(fill="x", pady=(8, 0)) |
| self._trk_special_note.configure(fg=FG3) |
|
|
| def _on_track_mode_change(self, *_): |
| if self._track_mode.get() == "auto": |
| self._track_manual_frame.pack_forget() |
| self._track_auto_frame.pack(fill="x") |
| else: |
| self._track_auto_frame.pack_forget() |
| self._track_manual_frame.pack(fill="x") |
|
|
| def _build_right(self, parent): |
| src = Section(parent, title="Fuente"); src.pack(fill="x", pady=(0, 10)) |
| tab_row = tk.Frame(src, bg=BG2); tab_row.pack(fill="x", pady=(0, 8)) |
| self._src_mode = tk.StringVar(value="url") |
| self._btn_tab_url = FlatButton(tab_row, text="URLs / Magnets", variant="primary", |
| command=lambda: self._switch_src("url")) |
| self._btn_tab_file = FlatButton(tab_row, text="Archivo / .torrent", |
| command=lambda: self._switch_src("file")) |
| self._btn_tab_url.pack(side="left") |
| self._btn_tab_file.pack(side="left", padx=(4, 0)) |
|
|
| self._src_type_lbl = tk.Label(src, text="", bg=BG2, fg=FG3, font=FONT_SM, anchor="w") |
| self._src_type_lbl.pack(anchor="w", pady=(0, 4)) |
|
|
| self._file_frame = tk.Frame(src, bg=BG2) |
| self._file_var = tk.StringVar() |
| self._file_var.trace_add("write", self._on_file_change) |
| FlatEntry(self._file_frame, textvariable=self._file_var).pack(side="left", fill="x", expand=True, ipady=4) |
| FlatButton(self._file_frame, text="examinar", command=self._browse).pack(side="right", padx=(6, 0)) |
| FlatButton(self._file_frame, text=".torrent", variant="torrent", command=self._browse_torrent).pack(side="right", padx=(4, 0)) |
|
|
| self._url_frame = tk.Frame(src, bg=BG2) |
| url_inner = tk.Frame(self._url_frame, bg=BG3, highlightthickness=1, highlightbackground=BORDER) |
| url_inner.pack(fill="x") |
| self._url_text = tk.Text(url_inner, bg=BG3, fg=FG, insertbackground=FG, |
| font=FONT, relief="flat", height=5, padx=8, pady=6) |
| self._url_text.bind("<KeyRelease>", self._on_url_change) |
| sb = ttk.Scrollbar(url_inner, command=self._url_text.yview) |
| self._url_text.configure(yscrollcommand=sb.set) |
| self._url_text.pack(side="left", fill="both", expand=True) |
| sb.pack(side="right", fill="y") |
|
|
| tk.Label(src, text="Soporta http/https - magnet:? - archivo.torrent", |
| bg=BG2, fg=FG3, font=FONT_SM).pack(anchor="w", pady=(4, 0)) |
|
|
| self._torrent_frame = tk.Frame(src, bg=BG2) |
| self._torrent_files_lbl = tk.Label(self._torrent_frame, text="", bg=BG2, fg=ORANGE, |
| font=FONT_SM, anchor="w", wraplength=600, justify="left") |
| self._torrent_files_lbl.pack(anchor="w", pady=(0, 4)) |
| self._btn_select_files = FlatButton(self._torrent_frame, text="seleccionar archivos", |
| variant="torrent", command=self._select_torrent_files) |
| self._btn_select_files.pack(anchor="w") |
|
|
| self._switch_src("url") |
|
|
| info_row = tk.Frame(src, bg=BG2, pady=6); info_row.pack(fill="x") |
| self._spinner_probe = Spinner(info_row, bg=BG2); self._spinner_probe.pack(side="left") |
| self._lbl_info = tk.Label(info_row, text="sin analizar", bg=BG2, fg=FG3, font=FONT_SM, anchor="w") |
| self._lbl_info.pack(side="left", padx=(6, 0)) |
| FlatButton(info_row, text="analizar", command=self._do_analyze).pack(side="right") |
|
|
| pg = Section(parent, title="Progreso"); pg.pack(fill="both", expand=True, pady=(0, 0)) |
| prog_row = tk.Frame(pg, bg=BG2); prog_row.pack(fill="x", pady=(0, 4)) |
| self._spinner_proc = Spinner(prog_row, bg=BG2); self._spinner_proc.pack(side="left") |
| self._lbl_prog = tk.Label(prog_row, text="esperando", bg=BG2, fg=FG3, font=FONT_SM, anchor="w") |
| self._lbl_prog.pack(side="left", padx=(6, 0)) |
| self._lbl_queue = tk.Label(prog_row, text="", bg=BG2, fg=FG3, font=FONT_SM) |
| self._lbl_queue.pack(side="right") |
| self._pbar = ttk.Progressbar(pg, mode="determinate", style="Horizontal.TProgressbar") |
| self._pbar.pack(fill="x", pady=(0, 8)) |
| log_wrap = tk.Frame(pg, bg=BG3, highlightthickness=1, highlightbackground=BORDER) |
| log_wrap.pack(fill="both", expand=True) |
| self._log_txt = tk.Text(log_wrap, bg=BG3, fg="#4ade80", font=("Consolas", 8), |
| relief="flat", state="disabled", padx=10, pady=8, cursor="arrow") |
| sb2 = ttk.Scrollbar(log_wrap, command=self._log_txt.yview) |
| self._log_txt.configure(yscrollcommand=sb2.set) |
| sb2.pack(side="right", fill="y") |
| self._log_txt.pack(fill="both", expand=True) |
|
|
| btn_row = tk.Frame(parent, bg=BG); btn_row.pack(fill="x", pady=(8, 0)) |
| self._btn_cancel = FlatButton(btn_row, text="CANCELAR", variant="ghost", |
| command=self._do_cancel, font=FONT_LG, pady=10) |
| self._btn_cancel.pack(side="right", padx=(6, 0)) |
| self._btn_cancel.configure(state="disabled") |
| self._btn_proc = FlatButton(btn_row, text="PROCESAR COLA", variant="primary", |
| command=self._do_process, font=FONT_LG, pady=10) |
| self._btn_proc.pack(side="left", fill="x", expand=True) |
|
|
| |
| def _update_aria2_status(self): |
| global ARIA2_PATH, ARIA2_OK |
| ARIA2_PATH = get_aria2_path() |
| ARIA2_OK = ARIA2_PATH is not None |
| if ARIA2_OK: |
| self._aria2_dot.configure(fg="#22c55e") |
| self._aria2_lbl.configure(text="aria2:ok", fg="#22c55e") |
| self._aria2_status_lbl.configure(text=f"aria2c encontrado: {ARIA2_PATH}", fg="#22c55e") |
| else: |
| self._aria2_dot.configure(fg=FG3) |
| self._aria2_lbl.configure(text="aria2:-", fg=FG3) |
| self._aria2_status_lbl.configure(text="aria2c no encontrado - coloca aria2c.exe junto al script", fg=YELLOW) |
|
|
| def _update_hf_status(self): |
| if HF_OK: |
| self._hf_dot.configure(fg=GREEN) |
| self._hf_lbl.configure(text="hf:ok", fg=GREEN) |
| else: |
| self._hf_dot.configure(fg=RED) |
| self._hf_lbl.configure(text="hf:no", fg=RED) |
|
|
| def _check_aria2(self): |
| self._log("\n[->] Verificando aria2c...") |
| self._update_aria2_status() |
| if ARIA2_OK: |
| self._log(f"[OK] aria2c encontrado: {ARIA2_PATH}") |
| else: |
| self._log("[X] aria2c no encontrado") |
| self._log(" Descarga de: https://github.com/aria2/aria2/releases") |
|
|
| |
| def _connect_hf(self): |
| t = self._hf_token.get().strip() |
| if not t: self._log("[!] Ingresa token"); return |
| self._spinner_hf.start() |
| self._btn_connect.configure(state="disabled") |
| self._log("[->] Verificando token HuggingFace...") |
| def _go(): |
| try: |
| if not HF_OK: raise ImportError("huggingface_hub no instalado") |
| api = HfApi() |
| info = api.whoami(token=t) |
| self.after(0, lambda: self._log(f"[OK] Conectado: {info.get('name')}")) |
| self.after(0, self._load_repos) |
| except Exception as e: |
| self.after(0, lambda: self._log(f"[X] {e}")) |
| finally: |
| self.after(0, self._spinner_hf.stop) |
| self.after(0, lambda: self._btn_connect.configure(state="normal")) |
| threading.Thread(target=_go, daemon=True).start() |
|
|
| def _load_repos(self): |
| t = self._hf_token.get().strip() |
| if not t or not HF_OK: return |
| self._spinner_hf.start() |
| self._log("[->] Cargando repositorios...") |
| def _go(): |
| try: |
| api = HfApi() |
| whoami = api.whoami(token=t) |
| author = whoami.get("name", "") |
| repos = [m.modelId for m in list(api.list_models(author=author, token=t))] |
| try: |
| repos += [d.id for d in list(api.list_datasets(author=author, token=t))] |
| except Exception: |
| pass |
| self.after(0, lambda: self._repo_cb.configure(values=repos)) |
| if repos: self.after(0, lambda: self._repo_cb.set(repos[0])) |
| self.after(0, lambda: self._log(f"[OK] {len(repos)} repositorios")) |
| except Exception as e: |
| self.after(0, lambda: self._log(f"[X] {e}")) |
| finally: |
| self.after(0, self._spinner_hf.stop) |
| threading.Thread(target=_go, daemon=True).start() |
|
|
| |
| def _tmdb_clear(self, *_): |
| 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): |
| q = self._tmdb_query.get().strip() |
| if not q: self._log("[!] Ingresa busqueda"); return |
| t = "movie" if self._tmdb_type.get() == "pelicula" else "tv" |
| url = (f"https://api.themoviedb.org/3/search/{t}?api_key={TMDB_API_KEY}" |
| f"&query={urllib.parse.quote(q)}&language=es-MX") |
| self._spinner_tmdb.start() |
| 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"[X] TMDb: {e}")) |
| finally: |
| self.after(0, self._spinner_tmdb.stop) |
| threading.Thread(target=_go, daemon=True).start() |
|
|
| def _on_tmdb_res_select(self, *_): |
| 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") |
| tid = self._tmdb_id_map.get(self._tmdb_res_cb.get()) |
| if not tid: return |
| url = f"https://api.themoviedb.org/3/tv/{tid}?api_key={TMDB_API_KEY}&language=es-MX" |
| self._spinner_tmdb.start() |
| 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"[X] TMDb: {e}")) |
| finally: |
| self.after(0, self._spinner_tmdb.stop) |
| threading.Thread(target=_go, daemon=True).start() |
|
|
| def _on_tmdb_season_select(self, *_): |
| 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] if self._tmdb_season_cb.get() else "1" |
| url = f"https://api.themoviedb.org/3/tv/{tid}/season/{s_num}?api_key={TMDB_API_KEY}&language=es-MX" |
| self._spinner_tmdb.start() |
| 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']:02d}: {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"[X] TMDb: {e}")) |
| finally: |
| self.after(0, self._spinner_tmdb.stop) |
| threading.Thread(target=_go, daemon=True).start() |
|
|
| |
| def _switch_src(self, mode): |
| self._src_mode.set(mode) |
| self._torrent_info = None |
| self._selected_files = None |
| self._torrent_frame.pack_forget() |
| if mode == "file": |
| self._url_frame.pack_forget() |
| self._file_frame.pack(fill="x", pady=(0, 6)) |
| self._btn_tab_file.configure(bg=BLUE, fg="white") |
| self._btn_tab_url.configure(bg=BG3, fg=FG2) |
| else: |
| self._file_frame.pack_forget() |
| self._url_frame.pack(fill="x", pady=(0, 6)) |
| self._btn_tab_url.configure(bg=BLUE, fg="white") |
| self._btn_tab_file.configure(bg=BG3, fg=FG2) |
|
|
| def _browse(self): |
| p = filedialog.askopenfilename( |
| filetypes=[("Video/Torrent", "*.mp4 *.mkv *.avi *.mov *.ts *.torrent"), ("Todos", "*.*")]) |
| if p: self._file_var.set(p) |
|
|
| def _browse_torrent(self): |
| p = filedialog.askopenfilename(filetypes=[("Torrent", "*.torrent"), ("Todos", "*.*")]) |
| if p: self._file_var.set(p); self._switch_src("file") |
|
|
| def _on_file_change(self, *_): |
| p = self._file_var.get().strip() |
| self._torrent_info = None |
| self._selected_files = None |
| self._torrent_frame.pack_forget() |
| if p.endswith(".torrent"): |
| self._src_type_lbl.configure(text="[torrent] detectado", fg=ORANGE) |
| elif p: |
| self._src_type_lbl.configure(text="[video] archivo de video", fg=GREEN) |
| else: |
| self._src_type_lbl.configure(text="", fg=FG3) |
|
|
| def _on_url_change(self, *_): |
| self._torrent_info = None |
| self._selected_files = None |
| self._torrent_frame.pack_forget() |
| lines = [ln.strip() for ln in self._url_text.get("1.0", "end").split("\n") if ln.strip()] |
| if not lines: self._src_type_lbl.configure(text="", fg=FG3); return |
| types = [] |
| if any(l.lower().startswith("magnet:") for l in lines): types.append("magnet") |
| if any(l.lower().endswith(".torrent") for l in lines): types.append(".torrent") |
| if any(l.startswith("http") for l in lines): types.append("http/s") |
| col = ORANGE if ("magnet" in types or ".torrent" in types) else GREEN |
| self._src_type_lbl.configure(text=f"[{' + '.join(types)}] ({len(lines)} elem)", fg=col) |
|
|
| def _get_sources(self): |
| if self._src_mode.get() == "file": |
| p = self._file_var.get().strip() |
| return ([p] if p else []), False |
| else: |
| return [ln.strip() for ln in self._url_text.get("1.0", "end").split("\n") if ln.strip()], True |
|
|
| |
| def _log(self, msg): |
| self._log_txt.configure(state="normal") |
| if any(k in msg for k in ["aria2", "ARIA2"]): |
| self._log_txt.tag_configure("aria2", foreground="#4ade80") |
| self._log_txt.insert("end", msg + "\n", "aria2") |
| elif any(k in msg for k in ["torrent", "magnet", "P2P", "metadata", "Metadata"]): |
| self._log_txt.tag_configure("t", foreground=ORANGE) |
| self._log_txt.insert("end", msg + "\n", "t") |
| elif any(k in msg for k in ["[OK]"]): |
| self._log_txt.tag_configure("ok", foreground=GREEN) |
| self._log_txt.insert("end", msg + "\n", "ok") |
| elif any(k in msg for k in ["[X]", "Error", "fallo"]): |
| self._log_txt.tag_configure("e", foreground=RED) |
| self._log_txt.insert("end", msg + "\n", "e") |
| elif any(k in msg for k in ["[modo]", "Todos+", "coreano", "inglΓ©s", "VTT", "vtt"]): |
| self._log_txt.tag_configure("special", foreground=PURPLE) |
| self._log_txt.insert("end", msg + "\n", "special") |
| else: |
| 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 |
| m = self._mode.get() if hasattr(self, '_mode') else "" |
| if "aria2" in label.lower(): |
| self._pbar.configure(style="Aria2.Horizontal.TProgressbar") |
| elif m == "Todos + Coreano": |
| self._pbar.configure(style="Korean.Horizontal.TProgressbar") |
| else: |
| self._pbar.configure(style="Horizontal.TProgressbar") |
| self._lbl_prog.configure(text=label) |
| self.update_idletasks() |
|
|
| |
| def _get_track_indices(self): |
| ai = 0 |
| si = 0 |
| if self._track_mode.get() == "manual": |
| try: |
| ai = int(self._manual_audio_idx.get().strip()) |
| except (ValueError, AttributeError): |
| ai = 0 |
| try: |
| si = int(self._manual_sub_idx.get().strip()) |
| except (ValueError, AttributeError): |
| si = 0 |
| else: |
| try: |
| if self._aud_cb.get(): |
| ai = int(self._aud_cb.get().split("]")[0].strip("[")) |
| except (ValueError, IndexError): |
| ai = 0 |
| try: |
| if self._sub_cb.get(): |
| si = int(self._sub_cb.get().split("]")[0].strip("[")) |
| except (ValueError, IndexError): |
| si = 0 |
| return ai, si |
|
|
| |
| def _do_analyze(self): |
| sources, _ = self._get_sources() |
| if not sources: |
| messagebox.showwarning("vcpro", "Selecciona fuente.") |
| return |
| src = sources[0] |
| |
| if src.strip().lower().startswith("magnet:"): |
| if not ARIA2_OK: |
| self._lbl_info.configure(text="[X] aria2c no encontrado", fg=RED) |
| self._log("[X] aria2c no encontrado. No se puede analizar magnet sin aria2") |
| return |
| self._spinner_probe.start() |
| self._lbl_info.configure(text="obteniendo metadata del magnet...", fg=ORANGE) |
| self._log(f"\n[->] Analizando magnet: {src[:80]}...") |
| def _go(): |
| info = get_magnet_info(src, lambda m: self.after(0, self._log, m), |
| lambda p, l: self.after(0, self._set_progress, p, l), |
| self._cancel_flag) |
| if info: |
| self._torrent_info = info |
| self._selected_files = None |
| videos = [f for f in info['files'] if Path(f['path']).suffix.lower() in VIDEO_EXTS] |
| txt = f"{info['name']} | {len(info['files'])} archivos | {len(videos)} videos" |
| self.after(0, lambda: self._lbl_info.configure(text=txt, fg=ORANGE)) |
| file_list = "\n".join([f" [{f['index']}] {f['path']} ({fmt_size(f['length'])})" |
| for f in info['files'][:15]]) |
| if len(info['files']) > 15: |
| file_list += f"\n ... y {len(info['files'])-15} mas" |
| self.after(0, lambda: self._torrent_files_lbl.configure(text=f"Archivos en torrent:\n{file_list}")) |
| self.after(0, lambda: self._torrent_frame.pack(fill="x", pady=(4, 0))) |
| self.after(0, lambda: self._aud_cb.configure(values=[])) |
| self.after(0, lambda: self._sub_cb.configure(values=[])) |
| else: |
| self.after(0, lambda: self._lbl_info.configure(text="[X] No se pudo obtener metadata", fg=RED)) |
| self.after(0, self._spinner_probe.stop) |
| self.after(0, lambda: self._set_progress(0, "")) |
| threading.Thread(target=_go, daemon=True).start() |
| return |
| |
| if src.strip().lower().endswith(".torrent"): |
| self._lbl_info.configure(text="[.torrent] los archivos se muestran al procesar", fg=ORANGE) |
| self._log("[->] Archivo .torrent detectado. Archivos se muestran al procesar.") |
| return |
| |
| self._spinner_probe.start() |
| self._lbl_info.configure(text="analizando...", fg=FG3) |
| def _go(): |
| info = probe(src) |
| 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])) |
| txt = (f"{info['title'] or 'sin titulo'} - {fmt_dur(info['duration'])} - " |
| f"{len(ac)}aud - {len(sc)}sub") |
| self.after(0, lambda: self._lbl_info.configure(text=txt, fg=GREEN)) |
| self.after(0, self._spinner_probe.stop) |
| threading.Thread(target=_go, daemon=True).start() |
|
|
| def _select_torrent_files(self): |
| if not self._torrent_info: |
| messagebox.showinfo("vcpro", "Primero analiza el magnet link") |
| return |
| dialog = TorrentFilesDialog(self, self._torrent_info) |
| self.wait_window(dialog) |
| if dialog.result is not None: |
| self._selected_files = dialog.result |
| videos = [i for i in dialog.result |
| if i < len(self._torrent_info['files']) |
| and Path(self._torrent_info['files'][i]['path']).suffix.lower() in VIDEO_EXTS] |
| self._log(f"[OK] Seleccionados {len(dialog.result)} archivos ({len(videos)} videos)") |
| self._lbl_info.configure(text=f"Seleccionados: {len(videos)} videos", fg=GREEN) |
| else: |
| self._log("[!] Seleccion cancelada") |
| self._selected_files = None |
|
|
| |
| def _do_cancel(self): |
| self._cancel_flag[0] = True |
| self._log("[!] Cancelando...") |
| self._btn_cancel.configure(state="disabled") |
|
|
| |
| def _do_process(self): |
| tok, rep = self._hf_token.get().strip(), self._repo_cb.get().strip() |
| sources, is_url = self._get_sources() |
| if not tok or not rep or not sources: |
| messagebox.showwarning("vcpro", "Faltan datos (token, repo o fuente).") |
| return |
| if is_torrent_source(sources[0]) and not ARIA2_OK: |
| messagebox.showwarning("vcpro", "aria2c no encontrado. Coloca aria2c.exe junto al script.") |
| return |
| |
| ai, si = self._get_track_indices() |
| m = self._mode.get() |
| if m in ("Todos + InglΓ©s", "Todos + Coreano"): |
| self._log(f"[pistas] Modo especial: {m} | pistas seleccionadas automaticamente") |
| else: |
| self._log(f"[pistas] Modo: {self._track_mode.get()} | Audio indice: {ai} | Sub indice: {si}") |
| |
| self._cancel_flag = [False] |
| self._queue = sources.copy() |
| self._total_queue = len(self._queue) |
| self._current_idx = 0 |
| self._btn_proc.configure(state="disabled", bg=FG3) |
| self._btn_cancel.configure(state="normal") |
| self._log_txt.configure(state="normal") |
| self._log_txt.delete("1.0", "end") |
| self._log_txt.configure(state="disabled") |
| self._spinner_proc.start() |
| self._process_next() |
|
|
| def _process_next(self): |
| if self._cancel_flag[0] or not self._queue: |
| self._btn_proc.configure(state="normal", bg=BLUE) |
| self._btn_cancel.configure(state="disabled") |
| self._lbl_queue.configure(text="") |
| self._spinner_proc.stop() |
| if self._cancel_flag[0]: |
| self._set_progress(0, "cancelado") |
| self._log("\n[X] CANCELADO") |
| else: |
| self._set_progress(100, "completado") |
| self._log("\n[OK] COLA FINALIZADA") |
| return |
|
|
| src = self._queue.pop(0) |
| self._current_idx += 1 |
| self._lbl_queue.configure(text=f"{self._current_idx}/{self._total_queue}", fg=FG2) |
| self._log(f"\n{'='*50}") |
| self._log(f"[{self._current_idx}/{self._total_queue}] {src[:60]}...") |
| self._log(f"{'='*50}") |
|
|
| folder_name = "Bulk_Upload" |
| file_name = f"Video_{self._current_idx}" |
|
|
| if self._use_tmdb.get() and self._tmdb_res_cb.get(): |
| base = self._tmdb_res_cb.get().split(" (")[0] |
| if self._tmdb_type.get() == "pelicula": |
| folder_name = to_leet(base) |
| file_name = (to_leet(f"{base} Parte {self._current_idx}") |
| if self._total_queue > 1 else to_leet(base)) |
| else: |
| s_num = self._tmdb_season_cb.get().split(" ")[1] if self._tmdb_season_cb.get() else "1" |
| folder_name = to_leet(f"{base} S{int(s_num):02d}") |
| tgt = max(self._tmdb_ep_cb.current(), 0) + self._current_idx - 1 |
| if tgt < len(self._tmdb_episodes): |
| ep = self._tmdb_episodes[tgt] |
| file_name = to_leet(f"{base} S{int(s_num):02d}E{ep['num']:02d} {ep['name']}") |
| else: |
| file_name = to_leet(f"{base} S{int(s_num):02d}E{tgt+1:02d}") |
| else: |
| m = self._manual_name.get().strip() |
| if m: |
| folder_name = to_leet(m) |
| file_name = to_leet(f"{m} {self._current_idx}") if self._total_queue > 1 else to_leet(m) |
| elif self._src_mode.get() == "file" and not is_torrent_source(src): |
| base = Path(src).stem |
| folder_name = to_leet(base) |
| file_name = to_leet(base) |
|
|
| ai, si = self._get_track_indices() |
| _, is_url_chk = self._get_sources() |
| sel_files = self._selected_files if is_torrent_source(src) else None |
|
|
| threading.Thread( |
| target=process_video, |
| args=( |
| self._hf_token.get().strip(), |
| self._repo_cb.get().strip(), |
| src, is_url_chk, self._mode.get(), |
| ai, self._gen_single.get(), |
| self._ext_sub.get(), si, |
| self._del_local.get(), |
| folder_name, file_name, sel_files, |
| lambda msg: self.after(0, self._log, msg), |
| lambda p, l: self.after(0, self._set_progress, p, l), |
| lambda ok: self.after(0, self._process_next), |
| self._cancel_flag, |
| ), |
| daemon=True |
| ).start() |
|
|
| |
| def run_installer_and_restart(): |
| import sys |
| root = tk.Tk() |
| root.title("vcpro - Setup") |
| root.geometry("460x200") |
| root.configure(bg=BG) |
| root.resizable(False, False) |
| root.update_idletasks() |
| x = (root.winfo_screenwidth() // 2) - 230 |
| y = (root.winfo_screenheight() // 2) - 100 |
| root.geometry(f"+{x}+{y}") |
| tk.Label(root, text="vcpro", bg=BG, fg=FG, font=FONT_XL).pack(pady=(20, 4)) |
| lbl = tk.Label(root, text="Instalando dependencias...", bg=BG, fg=FG2, font=FONT) |
| lbl.pack() |
| sp = Spinner(root, size=24, color=BLUE, bg=BG) |
| sp.pack(pady=10) |
| sp.start() |
| def go(): |
| try: |
| creationflags = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0 |
| p = subprocess.Popen( |
| [sys.executable, "-m", "pip", "install", "huggingface_hub"], |
| stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, |
| creationflags=creationflags |
| ) |
| for l in p.stdout: |
| if any(k in l for k in ["Collecting", "Installing", "Successfully"]): |
| root.after(0, lbl.configure, {"text": l.strip()[:60]}) |
| p.wait() |
| if p.returncode == 0: |
| root.after(0, lbl.configure, {"text": "Listo. Reiniciando...", "fg": GREEN}) |
| time.sleep(1.5) |
| os.execv(sys.executable, ['python'] + sys.argv) |
| else: |
| root.after(0, sp.stop) |
| root.after(0, lbl.configure, {"text": "Error al instalar.", "fg": RED}) |
| except Exception as e: |
| root.after(0, sp.stop) |
| root.after(0, lbl.configure, {"text": str(e), "fg": RED}) |
| threading.Thread(target=go, daemon=True).start() |
| root.mainloop() |
| sys.exit() |
|
|
| |
| if __name__ == "__main__": |
| try: |
| import huggingface_hub |
| DEP = True |
| except ImportError: |
| DEP = False |
|
|
| if not DEP: |
| run_installer_and_restart() |
| else: |
| app = App() |
| if not shutil.which("ffmpeg"): |
| app._log("[!] ffmpeg no detectado en PATH. Las conversiones fallaran.") |
| if ARIA2_OK: |
| app._log(f"[OK] aria2c encontrado: {ARIA2_PATH}") |
| else: |
| app._log("[!] aria2c no encontrado. Coloca aria2c.exe junto al script para descargar torrents.") |
| app.mainloop() |