plis2 / hug.py
CineMax's picture
Update hug.py
5a1b585 verified
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
# ─── HUGGINGFACE ──────────────────────────────────────────────────────────────
HF_OK = False
try:
from huggingface_hub import HfApi
HF_OK = True
except ImportError:
pass
# ─── TMDB API KEY (HARDCODEADA) ──────────────────────────────────────────────
TMDB_API_KEY = "3633a0416ea666f002ec317f40bdcf58"
# ─── ARIA2C (UNICO BACKEND PARA TORRENTS) ────────────────────────────────────
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
# ─── TORRENT HELPERS ──────────────────────────────────────────────────────────
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")
# ─── PALETA ───────────────────────────────────────────────────────────────────
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")
# ─── UTILIDADES ───────────────────────────────────────────────────────────────
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
# ─── EXTRAER TODOS LOS SUBTITULOS COMO VTT (del video original/no renderizado) ──
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 ""
# Nombre descriptivo
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}")
# Intentar extraer directamente a VTT
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:
# Fallback: extraer como SRT y convertir a VTT manualmente
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():
# Convertir SRT -> VTT
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")
# SRT usa comas en timestamps, VTT usa puntos
# AdemΓ‘s VTT lleva cabecera WEBVTT
lines = content.splitlines()
vtt_lines = ["WEBVTT", ""]
i = 0
while i < len(lines):
line = lines[i].strip()
# Saltar numero de bloque SRT (solo dΓ­gitos)
if re.match(r'^\d+$', line):
i += 1
continue
# Timestamp SRT: 00:00:00,000 --> 00:00:00,000
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:
# Convertir , -> . en ms
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
# Agregar lΓ­neas de texto del bloque
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
# ─── PROCESO PRINCIPAL ────────────────────────────────────────────────────────
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")
# ── MODO ESPECIAL: Todos + InglΓ©s / Todos + Coreano ──────────────
# Estos modos generan DOS videos renderizados y extraen TODOS los subs del original
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")
# ── VIDEO 1: Todos los audios (sin filtrar) ──────────────────
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}")
# ── VIDEO 2: Solo audio del idioma target ────────────────────
# Buscar pistas del idioma target
target_audio_tracks = [a for a in info["audio"] if lang_target in a["lang"].lower()]
if not target_audio_tracks:
# Si no hay pista del idioma, usar la primera disponible
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}")
# ── EXTRAER TODOS LOS SUBS DEL VIDEO ORIGINAL (no renderizado) ──
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")
# Renombrar video ALL a nombre principal para claridad
# (ya tiene nombre descriptivo, estΓ‘ bien)
continue # Siguiente archivo fuente
# ── MODO NORMAL (Copy + MP3, Copy + FLAC, H264 1080p) ────────────
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")
# EXTRAER AUDIO del video PROCESADO (out_mp4)
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}")
# EXTRAER SUBTITULO del video PROCESADO (out_mp4)
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)
# ─── WIDGETS ─────────────────────────────────────────────────────────────────
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))
# ─── DIALOGO SELECCION ARCHIVOS TORRENT ──────────────────────────────────────
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()
# ─── APP ─────────────────────────────────────────────────────────────────────
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):
# ── HuggingFace ──────────────────────────────────────────────────────
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))
# ── TMDb ──────────────────────────────────────────────────────────────
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 = []
# ── Opciones ─────────────────────────────────────────────────────────
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")
# Fila 1: modos normales
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))
# Fila 2: modos especiales
mode_row2 = tk.Frame(opt, bg=BG2); mode_row2.pack(fill="x", pady=(2, 6))
# Todos + InglΓ©s
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))
# Todos + Coreano
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))
# Descripcion del modo seleccionado
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))
# ── Pistas: Auto / Manual ────────────────────────────────────────────
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))
# Nota sobre modos especiales
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))
# Auto frame
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))
# Manual frame
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")
# Checkboxes extraer (solo modos normales)
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 ────────────────────────────────────────────────────────────
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, ""))
# En modos especiales, ocultar checkboxes de extraccion individual (no aplican)
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)
# ── STATUS ────────────────────────────────────────────────────────────────
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")
# ── HF ────────────────────────────────────────────────────────────────────
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()
# ── TMDb ─────────────────────────────────────────────────────────────────
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()
# ── SRC ──────────────────────────────────────────────────────────────────
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
# ── LOG / PROGRESS ───────────────────────────────────────────────────────
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()
# ── OBTENER INDICES DE PISTAS ────────────────────────────────────────────
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
# ── ANALYZE ──────────────────────────────────────────────────────────────
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
# ── CANCEL ───────────────────────────────────────────────────────────────
def _do_cancel(self):
self._cancel_flag[0] = True
self._log("[!] Cancelando...")
self._btn_cancel.configure(state="disabled")
# ── PROCESS ──────────────────────────────────────────────────────────────
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()
# ─── INSTALLER ───────────────────────────────────────────────────────────────
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()
# ─── MAIN ─────────────────────────────────────────────────────────────────────
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()