Upload 3 files
Browse files- hls.py.txt +837 -0
- hug.bat +54 -0
- hug.py +540 -0
hls.py.txt
ADDED
|
@@ -0,0 +1,837 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
HLS/DASH Converter EXTREME — App nativa Windows
|
| 3 |
+
Convierte a HLS/DASH y sube a GitHub. Sin Gradio.
|
| 4 |
+
Solución definitiva error Git 128 y Token.
|
| 5 |
+
"""
|
| 6 |
+
import sys
|
| 7 |
+
import os
|
| 8 |
+
import ctypes
|
| 9 |
+
import subprocess
|
| 10 |
+
import threading
|
| 11 |
+
import tempfile
|
| 12 |
+
import shutil
|
| 13 |
+
import re
|
| 14 |
+
import json
|
| 15 |
+
import time
|
| 16 |
+
import tkinter as tk
|
| 17 |
+
from tkinter import ttk, filedialog, messagebox
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 20 |
+
|
| 21 |
+
# ─── AUTO-ELEVACIÓN ADMINISTRADOR ───────────────────────────
|
| 22 |
+
def is_admin():
|
| 23 |
+
try:
|
| 24 |
+
return ctypes.windll.shell32.IsUserAnAdmin()
|
| 25 |
+
except:
|
| 26 |
+
return False
|
| 27 |
+
|
| 28 |
+
def elevate():
|
| 29 |
+
if not is_admin():
|
| 30 |
+
ctypes.windll.shell32.ShellExecuteW(
|
| 31 |
+
None, "runas", sys.executable, " ".join(sys.argv), None, 1
|
| 32 |
+
)
|
| 33 |
+
sys.exit()
|
| 34 |
+
|
| 35 |
+
elevate()
|
| 36 |
+
|
| 37 |
+
# ─── AUTO-INSTALACIÓN DEPENDENCIAS ──────────────────────────
|
| 38 |
+
try:
|
| 39 |
+
import requests
|
| 40 |
+
except ImportError:
|
| 41 |
+
print("Instalando 'requests'...")
|
| 42 |
+
try:
|
| 43 |
+
subprocess.check_call([sys.executable, "-m", "pip", "install", "requests", "--user", "-q"])
|
| 44 |
+
import requests
|
| 45 |
+
except Exception as e:
|
| 46 |
+
print(f"Error instalando requests: {e}")
|
| 47 |
+
input("Presiona Enter para salir...")
|
| 48 |
+
sys.exit(1)
|
| 49 |
+
|
| 50 |
+
# ─── COLORES ────────────────────────────────────────────────
|
| 51 |
+
BG = "#080d18"
|
| 52 |
+
BG2 = "#0f1629"
|
| 53 |
+
BG3 = "#17213a"
|
| 54 |
+
ACCENT = "#6366f1"
|
| 55 |
+
ACCENT2 = "#8b5cf6"
|
| 56 |
+
GREEN = "#22c55e"
|
| 57 |
+
RED = "#ef4444"
|
| 58 |
+
YELLOW = "#f59e0b"
|
| 59 |
+
CYAN = "#22d3ee"
|
| 60 |
+
FG = "#e2e8f0"
|
| 61 |
+
FG2 = "#94a3b8"
|
| 62 |
+
FG3 = "#3a5070"
|
| 63 |
+
BORDER = "#1a2a40"
|
| 64 |
+
FM = ("Consolas", 10)
|
| 65 |
+
FB = ("Consolas", 11, "bold")
|
| 66 |
+
FS = ("Consolas", 9)
|
| 67 |
+
|
| 68 |
+
# ─── UTILS ──────────────────────────────────────────────────
|
| 69 |
+
def get_threads():
|
| 70 |
+
try:
|
| 71 |
+
return max(len(os.sched_getaffinity(0)) - 1, 1)
|
| 72 |
+
except:
|
| 73 |
+
return max((os.cpu_count() or 4) - 1, 1)
|
| 74 |
+
|
| 75 |
+
N_THREADS = get_threads()
|
| 76 |
+
|
| 77 |
+
def clean_folder(n): return re.sub(r'[<>:"/\\|?*]', '_', n).strip()[:200]
|
| 78 |
+
def clean_repo(n):
|
| 79 |
+
n = re.sub(r'[^a-zA-Z0-9_-]', '-', n)
|
| 80 |
+
return re.sub(r'-+', '-', n).strip('-')[:100]
|
| 81 |
+
|
| 82 |
+
def build_name(ctype, mname, sname, season, ep_start, idx):
|
| 83 |
+
if ctype == "Película":
|
| 84 |
+
b = mname.strip() or "Pelicula"
|
| 85 |
+
return clean_folder(b), clean_repo(b)
|
| 86 |
+
name = sname.strip() or "Serie"
|
| 87 |
+
try: t = int(season)
|
| 88 |
+
except: t = 1
|
| 89 |
+
try: e = int(ep_start) + idx
|
| 90 |
+
except: e = idx + 1
|
| 91 |
+
lbl = f"{name}_T{t}_Ep{e}"
|
| 92 |
+
return clean_folder(lbl), clean_repo(lbl)
|
| 93 |
+
|
| 94 |
+
def is_live(url):
|
| 95 |
+
if not url.startswith(("http://", "https://")): return False
|
| 96 |
+
lo = url.lower()
|
| 97 |
+
if any(lo.endswith(x) for x in ['.mp4','.mkv','.avi','.mov','.ts']): return False
|
| 98 |
+
return any(p in lo for p in ['/live/','/stream/','.m3u8','.mpd','/hls/','/dash/'])
|
| 99 |
+
|
| 100 |
+
def input_args(src):
|
| 101 |
+
ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
| 102 |
+
if src.startswith(("http://", "https://")):
|
| 103 |
+
if is_live(src):
|
| 104 |
+
return ['-user_agent', ua, '-timeout', '60000000',
|
| 105 |
+
'-reconnect','1','-reconnect_streamed','1',
|
| 106 |
+
'-fflags','+genpts+igndts','-probesize','100M','-analyzeduration','50M']
|
| 107 |
+
return ['-user_agent', ua, '-timeout', '120000000',
|
| 108 |
+
'-reconnect','1','-reconnect_streamed','1','-reconnect_delay_max','10',
|
| 109 |
+
'-analyzeduration','500000000','-probesize','500000000',
|
| 110 |
+
'-fflags','+genpts+igndts','-err_detect','ignore_err',
|
| 111 |
+
'-max_delay','10000000','-rw_timeout','120000000']
|
| 112 |
+
return ['-analyzeduration','100000000','-probesize','100000000','-fflags','+genpts+igndts']
|
| 113 |
+
|
| 114 |
+
def render_args():
|
| 115 |
+
return ['-threads', str(N_THREADS), '-thread_type', 'slice+frame',
|
| 116 |
+
'-slices', str(N_THREADS), '-max_muxing_queue_size', '9999',
|
| 117 |
+
'-thread_queue_size', '4096']
|
| 118 |
+
|
| 119 |
+
def detect_duration(src, iargs):
|
| 120 |
+
try:
|
| 121 |
+
cmd = ['ffprobe','-v','error'] + iargs + ['-show_entries','format=duration','-of','json','-i',src]
|
| 122 |
+
r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
| 123 |
+
d = json.loads(r.stdout).get('format',{}).get('duration')
|
| 124 |
+
return float(d) if d and d != 'N/A' else None
|
| 125 |
+
except: return None
|
| 126 |
+
|
| 127 |
+
def detect_audio(src, iargs):
|
| 128 |
+
try:
|
| 129 |
+
cmd = ['ffprobe','-v','error'] + iargs + [
|
| 130 |
+
'-select_streams','a',
|
| 131 |
+
'-show_entries','stream=index,codec_name:stream_tags=language,title',
|
| 132 |
+
'-of','json','-i',src]
|
| 133 |
+
r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
| 134 |
+
tracks = []
|
| 135 |
+
for s in json.loads(r.stdout).get('streams',[]):
|
| 136 |
+
tags = s.get('tags',{})
|
| 137 |
+
tracks.append({'index': s.get('index',0),
|
| 138 |
+
'language': tags.get('language','und').lower(),
|
| 139 |
+
'title': tags.get('title', f"Audio {s.get('index',0)}"),
|
| 140 |
+
'codec': s.get('codec_name','unknown')})
|
| 141 |
+
return tracks
|
| 142 |
+
except: return []
|
| 143 |
+
|
| 144 |
+
def prioritize_audio(tracks):
|
| 145 |
+
prio = ['spa','es','spanish','español','latino','lat','es-mx','es-419']
|
| 146 |
+
def key(t):
|
| 147 |
+
l, ti = t['language'], t['title'].lower()
|
| 148 |
+
for p in prio:
|
| 149 |
+
if p in l or p in ti: return 0
|
| 150 |
+
return 1 if l == 'und' else 2
|
| 151 |
+
return sorted(tracks, key=key)
|
| 152 |
+
|
| 153 |
+
def make_master_m3u8(out_dir, video_streams, audio_playlists):
|
| 154 |
+
c = "#EXTM3U\n#EXT-X-VERSION:7\n\n"
|
| 155 |
+
for a in audio_playlists:
|
| 156 |
+
d = "YES" if a['is_default'] else "NO"
|
| 157 |
+
c += f'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="{a["title"]}",LANGUAGE="{a["language"]}",DEFAULT={d},AUTOSELECT={d},URI="{a["file"]}"\n'
|
| 158 |
+
c += "\n"
|
| 159 |
+
for v in video_streams:
|
| 160 |
+
c += f'#EXT-X-STREAM-INF:BANDWIDTH={v["bandwidth"]},RESOLUTION={v["resolution"]},CODECS="{v["codecs"]}",AUDIO="audio"\n{v["file"]}\n'
|
| 161 |
+
(out_dir / "master.m3u8").write_text(c)
|
| 162 |
+
|
| 163 |
+
RAMDISK_PATH = None
|
| 164 |
+
def setup_ramdisk():
|
| 165 |
+
global RAMDISK_PATH
|
| 166 |
+
for p in [r"C:\Temp\hls_tmp", tempfile.gettempdir() + r"\hls_tmp"]:
|
| 167 |
+
try:
|
| 168 |
+
Path(p).mkdir(parents=True, exist_ok=True)
|
| 169 |
+
RAMDISK_PATH = Path(p)
|
| 170 |
+
return Path(p)
|
| 171 |
+
except: continue
|
| 172 |
+
RAMDISK_PATH = Path(tempfile.gettempdir())
|
| 173 |
+
return RAMDISK_PATH
|
| 174 |
+
|
| 175 |
+
RAMDISK = setup_ramdisk()
|
| 176 |
+
|
| 177 |
+
def process_audio_track(args):
|
| 178 |
+
track, i, total, src, out_dir, iargs = args
|
| 179 |
+
tmp = RAMDISK / f"aud_{os.getpid()}_{i}_{int(time.time())}"
|
| 180 |
+
tmp.mkdir(exist_ok=True)
|
| 181 |
+
try:
|
| 182 |
+
af = tmp / f"audio_{i}.m3u8"
|
| 183 |
+
sf = tmp / f"audio_{i}_%03d.ts"
|
| 184 |
+
wav_tmp = tmp / f"audio_{i}_raw.wav"
|
| 185 |
+
|
| 186 |
+
r_dec = subprocess.run(
|
| 187 |
+
['ffmpeg','-hide_banner','-threads',str(N_THREADS)] + iargs + [
|
| 188 |
+
'-i',src,'-map',f"0:{track['index']}",'-vn',
|
| 189 |
+
'-c:a','pcm_s16le','-ar','48000','-ac','2','-f','wav',
|
| 190 |
+
str(wav_tmp),'-y','-loglevel','error'],
|
| 191 |
+
capture_output=True, text=True, timeout=7200)
|
| 192 |
+
if r_dec.returncode != 0 or not wav_tmp.exists():
|
| 193 |
+
raise Exception(f"Decode PCM falló: {r_dec.stderr[-300:]}")
|
| 194 |
+
|
| 195 |
+
r_enc = subprocess.run(
|
| 196 |
+
['ffmpeg','-hide_banner','-threads',str(N_THREADS),
|
| 197 |
+
'-i',str(wav_tmp),'-c:a','libmp3lame','-b:a','192k',
|
| 198 |
+
'-ar','48000','-ac','2','-compression_level','0',
|
| 199 |
+
'-max_muxing_queue_size','9999','-thread_queue_size','4096',
|
| 200 |
+
'-hls_time','5','-hls_list_size','0',
|
| 201 |
+
'-hls_segment_filename',str(sf),
|
| 202 |
+
'-hls_flags','independent_segments+append_list+temp_file',
|
| 203 |
+
'-hls_playlist_type','vod','-hls_segment_type','mpegts',
|
| 204 |
+
str(af),'-y','-loglevel','error'],
|
| 205 |
+
capture_output=True, text=True, timeout=7200)
|
| 206 |
+
wav_tmp.unlink(missing_ok=True)
|
| 207 |
+
|
| 208 |
+
if r_enc.returncode != 0 or not af.exists():
|
| 209 |
+
raise Exception(f"Encode MP3 falló: {r_enc.stderr[-300:]}")
|
| 210 |
+
|
| 211 |
+
ts_files = list(tmp.glob(f"audio_{i}_*.ts"))
|
| 212 |
+
if not ts_files: raise Exception("Sin segmentos TS")
|
| 213 |
+
|
| 214 |
+
shutil.move(str(af), str(out_dir / f"audio_{i}.m3u8"))
|
| 215 |
+
for ts in ts_files: shutil.move(str(ts), str(out_dir / ts.name))
|
| 216 |
+
|
| 217 |
+
return {'success': True, 'index': i, 'track': track,
|
| 218 |
+
'segments_count': len(ts_files),
|
| 219 |
+
'playlist': {'file': f"audio_{i}.m3u8",
|
| 220 |
+
'language': track['language'],
|
| 221 |
+
'title': track['title'], 'is_default': i == 0}}
|
| 222 |
+
except Exception as e:
|
| 223 |
+
return {'success': False, 'index': i, 'error': str(e), 'track': track}
|
| 224 |
+
finally:
|
| 225 |
+
shutil.rmtree(tmp, ignore_errors=True)
|
| 226 |
+
|
| 227 |
+
def process_video_copy(src, iargs, out_dir, tmp_v, vf, sf, log_cb):
|
| 228 |
+
hls = ['-hls_time','5','-hls_list_size','0','-hls_segment_filename',str(sf),
|
| 229 |
+
'-hls_flags','independent_segments+append_list+temp_file+split_by_time',
|
| 230 |
+
'-hls_playlist_type','vod','-hls_segment_type','mpegts',str(vf),'-y','-loglevel','error']
|
| 231 |
+
base = ['ffmpeg','-hide_banner','-threads',str(N_THREADS)] + iargs + [
|
| 232 |
+
'-i',src,'-map','0:v:0','-an',
|
| 233 |
+
'-max_muxing_queue_size','9999','-avoid_negative_ts','make_zero',
|
| 234 |
+
'-fflags','+genpts+igndts+flush_packets+discardcorrupt',
|
| 235 |
+
'-err_detect','ignore_err','-copytb','1','-start_at_zero','-thread_queue_size','4096']
|
| 236 |
+
|
| 237 |
+
r1 = subprocess.run(base + ['-c:v','copy','-bsf:v','h264_mp4toannexb,dump_extra'] + hls,
|
| 238 |
+
capture_output=True, text=True, timeout=7200)
|
| 239 |
+
if r1.returncode == 0 and vf.exists() and list(tmp_v.glob("video_1080p_*.ts")):
|
| 240 |
+
log_cb(" ✓ Video COPY OK (h264_mp4toannexb)"); return True
|
| 241 |
+
|
| 242 |
+
vf.unlink(missing_ok=True)
|
| 243 |
+
for f in tmp_v.glob("video_1080p_*.ts"): f.unlink(missing_ok=True)
|
| 244 |
+
log_cb(" ⚠ Copy con bsf falló, reintentando sin bsf…")
|
| 245 |
+
|
| 246 |
+
r2 = subprocess.run(base + ['-c:v','copy'] + hls,
|
| 247 |
+
capture_output=True, text=True, timeout=7200)
|
| 248 |
+
if r2.returncode == 0 and vf.exists() and list(tmp_v.glob("video_1080p_*.ts")):
|
| 249 |
+
log_cb(" ✓ Video COPY OK (sin bsf)"); return True
|
| 250 |
+
|
| 251 |
+
vf.unlink(missing_ok=True)
|
| 252 |
+
for f in tmp_v.glob("video_1080p_*.ts"): f.unlink(missing_ok=True)
|
| 253 |
+
log_cb(" ⚠ Copy falló, re-encode H.264 ultrafast…")
|
| 254 |
+
|
| 255 |
+
hls2 = ['-hls_time','5','-hls_list_size','0','-hls_segment_filename',str(sf),
|
| 256 |
+
'-hls_flags','independent_segments+append_list+temp_file',
|
| 257 |
+
'-hls_playlist_type','vod','-hls_segment_type','mpegts',str(vf),'-y','-loglevel','error']
|
| 258 |
+
r3 = subprocess.run(
|
| 259 |
+
['ffmpeg','-hide_banner','-threads',str(N_THREADS)] + iargs + [
|
| 260 |
+
'-i',src,'-map','0:v:0','-an','-c:v','libx264','-preset','ultrafast','-crf','23',
|
| 261 |
+
'-pix_fmt','yuv420p','-threads',str(N_THREADS),
|
| 262 |
+
'-max_muxing_queue_size','9999','-thread_queue_size','4096'] + hls2,
|
| 263 |
+
capture_output=True, text=True, timeout=14400)
|
| 264 |
+
if r3.returncode == 0 and vf.exists() and list(tmp_v.glob("video_1080p_*.ts")):
|
| 265 |
+
log_cb(" ✓ Video re-encode H.264 OK"); return True
|
| 266 |
+
raise Exception(f"Video falló en todos los modos: {r3.stderr[-400:]}")
|
| 267 |
+
|
| 268 |
+
def run_cmd(cmd, cwd=None, check=True):
|
| 269 |
+
"""Ejecuta comando shell capturando errores"""
|
| 270 |
+
res = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
|
| 271 |
+
if check and res.returncode != 0:
|
| 272 |
+
raise Exception(f"CMD Error: {res.stderr.strip()[:500]}")
|
| 273 |
+
return res
|
| 274 |
+
|
| 275 |
+
def run_conversion(job, log_cb, progress_cb, done_cb):
|
| 276 |
+
token = job['token']
|
| 277 |
+
sources = job['sources']
|
| 278 |
+
conv_opt = job['conv_opt']
|
| 279 |
+
stream_fmt = job['stream_fmt']
|
| 280 |
+
batch_size = job['batch_size']
|
| 281 |
+
delete_local = job['delete_local']
|
| 282 |
+
max_workers = job['max_workers']
|
| 283 |
+
ctype = job['ctype']
|
| 284 |
+
mname = job['mname']
|
| 285 |
+
sname = job['sname']
|
| 286 |
+
season = job['season']
|
| 287 |
+
ep_start = job['ep_start']
|
| 288 |
+
|
| 289 |
+
final_links = []
|
| 290 |
+
try:
|
| 291 |
+
log_cb(f"🚀 EXTREME v6.0 | Threads: {N_THREADS} | Temp: {RAMDISK}")
|
| 292 |
+
|
| 293 |
+
for idx, source in enumerate(sources):
|
| 294 |
+
src = source['value']
|
| 295 |
+
folder_name, repo_name = build_name(ctype, mname, sname, season, ep_start, idx)
|
| 296 |
+
log_cb(f"\n📦 [{idx+1}/{len(sources)}] {folder_name}")
|
| 297 |
+
progress_cb(int(idx/len(sources)*100), f"[{idx+1}/{len(sources)}] {folder_name}")
|
| 298 |
+
|
| 299 |
+
out_dir = RAMDISK / "outputs" / folder_name
|
| 300 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 301 |
+
|
| 302 |
+
iargs = input_args(src)
|
| 303 |
+
dur = detect_duration(src, iargs)
|
| 304 |
+
if dur:
|
| 305 |
+
h, m, s2 = int(dur//3600), int((dur%3600)//60), int(dur%60)
|
| 306 |
+
log_cb(f" ⏱ Duración: {h:02d}:{m:02d}:{s2:02d}")
|
| 307 |
+
|
| 308 |
+
audio_tracks = detect_audio(src, iargs) or [{'index':0,'language':'und','title':'Audio','codec':'unk'}]
|
| 309 |
+
audio_tracks = prioritize_audio(audio_tracks)
|
| 310 |
+
log_cb(f" 🎵 {len(audio_tracks)} pista(s)")
|
| 311 |
+
|
| 312 |
+
manifest_file = ""
|
| 313 |
+
|
| 314 |
+
if stream_fmt == "HLS (M3U8)":
|
| 315 |
+
log_cb(f" 🎵 Procesando {len(audio_tracks)} audio(s) → PCM → MP3")
|
| 316 |
+
aargs_list = [(t, i, len(audio_tracks), src, out_dir, iargs) for i, t in enumerate(audio_tracks)]
|
| 317 |
+
audio_playlists = []
|
| 318 |
+
with ThreadPoolExecutor(max_workers=max_workers) as ex:
|
| 319 |
+
futures = {ex.submit(process_audio_track, a): a for a in aargs_list}
|
| 320 |
+
for fut in as_completed(futures):
|
| 321 |
+
res = fut.result()
|
| 322 |
+
if res['success']:
|
| 323 |
+
audio_playlists.append(res['playlist'])
|
| 324 |
+
log_cb(f" ✓ Audio {res['index']+1}: {res['track']['title']} ({res['segments_count']} segs)")
|
| 325 |
+
else:
|
| 326 |
+
log_cb(f" ✗ Audio {res['index']+1}: {res.get('error','')[:120]}")
|
| 327 |
+
|
| 328 |
+
audio_playlists.sort(key=lambda x: int(x['file'].split('_')[1].split('.')[0]))
|
| 329 |
+
if not audio_playlists: raise Exception("Sin audio procesado")
|
| 330 |
+
|
| 331 |
+
video_streams = []
|
| 332 |
+
if "Copy" in conv_opt:
|
| 333 |
+
log_cb(" ⚡ Video COPY → HLS")
|
| 334 |
+
tmp_v = RAMDISK / f"vid_{idx}"
|
| 335 |
+
tmp_v.mkdir(exist_ok=True)
|
| 336 |
+
vf = tmp_v / "video_1080p.m3u8"
|
| 337 |
+
sf = tmp_v / "video_1080p_%03d.ts"
|
| 338 |
+
process_video_copy(src, iargs, out_dir, tmp_v, vf, sf, log_cb)
|
| 339 |
+
final_1080 = out_dir / "video_1080p.m3u8"
|
| 340 |
+
shutil.move(str(vf), str(final_1080))
|
| 341 |
+
for ts in tmp_v.glob("video_1080p_*.ts"):
|
| 342 |
+
shutil.move(str(ts), str(out_dir / ts.name))
|
| 343 |
+
shutil.rmtree(tmp_v, ignore_errors=True)
|
| 344 |
+
(out_dir / "video_720p.m3u8").write_text(final_1080.read_text())
|
| 345 |
+
video_streams = [
|
| 346 |
+
{'file':'video_1080p.m3u8','resolution':'1920x1080','bandwidth':5000000,'codecs':'avc1.640028'},
|
| 347 |
+
{'file':'video_720p.m3u8', 'resolution':'1280x720', 'bandwidth':3000000,'codecs':'avc1.640028'},
|
| 348 |
+
]
|
| 349 |
+
else:
|
| 350 |
+
rlist = ([{'label':'4K','scale':'scale=-2:2160','br':'15000k','bufsize':'30000k','res':'3840x2160'},
|
| 351 |
+
{'label':'1080p','scale':'scale=-2:1080','br':'5000k','bufsize':'10000k','res':'1920x1080'},
|
| 352 |
+
{'label':'720p','scale':'scale=-2:720','br':'2800k','bufsize':'5600k','res':'1280x720'}]
|
| 353 |
+
if "4K" in conv_opt else
|
| 354 |
+
[{'label':'1080p','scale':'scale=-2:1080','br':'5000k','bufsize':'10000k','res':'1920x1080'},
|
| 355 |
+
{'label':'720p','scale':'scale=-2:720','br':'2800k','bufsize':'5600k','res':'1280x720'}])
|
| 356 |
+
for rc in rlist:
|
| 357 |
+
log_cb(f" 🔄 Renderizando {rc['label']}…")
|
| 358 |
+
tmp_r = RAMDISK / f"render_{rc['label']}"
|
| 359 |
+
tmp_r.mkdir(exist_ok=True)
|
| 360 |
+
vf = tmp_r / f"video_{rc['label']}.m3u8"
|
| 361 |
+
sf = tmp_r / f"video_{rc['label']}_%03d.ts"
|
| 362 |
+
r = subprocess.run(
|
| 363 |
+
['ffmpeg','-hide_banner','-threads',str(N_THREADS)] + iargs + [
|
| 364 |
+
'-i',src,'-map','0:v:0','-an','-c:v','libx264',
|
| 365 |
+
'-preset','ultrafast','-crf','23','-vf',rc['scale'],
|
| 366 |
+
'-b:v',rc['br'],'-maxrate',rc['br'],'-bufsize',rc['bufsize'],
|
| 367 |
+
'-pix_fmt','yuv420p'] + render_args() + [
|
| 368 |
+
'-hls_time','5','-hls_list_size','0',
|
| 369 |
+
'-hls_segment_filename',str(sf),
|
| 370 |
+
'-hls_flags','independent_segments+append_list+temp_file',
|
| 371 |
+
'-hls_playlist_type','vod','-hls_segment_type','mpegts',
|
| 372 |
+
str(vf),'-y','-loglevel','error'],
|
| 373 |
+
capture_output=True, text=True, timeout=14400)
|
| 374 |
+
if r.returncode != 0:
|
| 375 |
+
shutil.rmtree(tmp_r, ignore_errors=True)
|
| 376 |
+
log_cb(f" ⚠ Error {rc['label']}")
|
| 377 |
+
continue
|
| 378 |
+
shutil.move(str(vf), str(out_dir / f"video_{rc['label']}.m3u8"))
|
| 379 |
+
for ts in tmp_r.glob(f"video_{rc['label']}_*.ts"):
|
| 380 |
+
shutil.move(str(ts), str(out_dir / ts.name))
|
| 381 |
+
shutil.rmtree(tmp_r, ignore_errors=True)
|
| 382 |
+
video_streams.append({'file':f"video_{rc['label']}.m3u8",
|
| 383 |
+
'resolution':rc['res'],
|
| 384 |
+
'bandwidth':int(rc['br'].replace('k','000'))+192000,
|
| 385 |
+
'codecs':'avc1.640028'})
|
| 386 |
+
log_cb(f" ✓ {rc['label']} OK")
|
| 387 |
+
|
| 388 |
+
if not video_streams: raise Exception("Sin video generado")
|
| 389 |
+
make_master_m3u8(out_dir, video_streams, audio_playlists)
|
| 390 |
+
manifest_file = "master.m3u8"
|
| 391 |
+
|
| 392 |
+
elif stream_fmt == "DASH (MPD)":
|
| 393 |
+
log_cb(" 🎬 Generando DASH…")
|
| 394 |
+
tmp_d = RAMDISK / f"dash_{idx}"
|
| 395 |
+
tmp_d.mkdir(exist_ok=True)
|
| 396 |
+
cmd = ['ffmpeg','-hide_banner','-threads',str(N_THREADS)] + iargs + ['-i',src]
|
| 397 |
+
vc = "copy" if "Copy" in conv_opt else "libx264"
|
| 398 |
+
cmd += ['-map','0:v:0','-c:v:0',vc]
|
| 399 |
+
if vc != "copy": cmd += ['-preset','ultrafast','-crf','23']
|
| 400 |
+
for i2, t2 in enumerate(audio_tracks):
|
| 401 |
+
cmd += ['-map',f"0:{t2['index']}",f'-c:a:{i2}','aac',f'-b:a:{i2}','192k',
|
| 402 |
+
f'-ar:a:{i2}','48000',f'-ac:a:{i2}','2']
|
| 403 |
+
mpd_out = tmp_d / "manifest.mpd"
|
| 404 |
+
cmd += (render_args() if vc != "copy" else
|
| 405 |
+
['-max_muxing_queue_size','9999','-copytb','1','-start_at_zero','-thread_queue_size','4096'])
|
| 406 |
+
cmd += ['-f','dash','-seg_duration','5','-use_template','1','-use_timeline','0',
|
| 407 |
+
'-init_seg_name','init-$RepresentationID$.m4s',
|
| 408 |
+
'-media_seg_name','chunk-$RepresentationID$-$Number%05d$.m4s',
|
| 409 |
+
str(mpd_out),'-y','-loglevel','error']
|
| 410 |
+
r = subprocess.run(cmd, capture_output=True, text=True, timeout=14400)
|
| 411 |
+
if r.returncode != 0:
|
| 412 |
+
shutil.rmtree(tmp_d, ignore_errors=True)
|
| 413 |
+
raise Exception(f"DASH falló: {r.stderr[-400:]}")
|
| 414 |
+
shutil.move(str(mpd_out), str(out_dir / "manifest.mpd"))
|
| 415 |
+
for m4s in tmp_d.glob("*.m4s"): shutil.move(str(m4s), str(out_dir / m4s.name))
|
| 416 |
+
shutil.rmtree(tmp_d, ignore_errors=True)
|
| 417 |
+
manifest_file = "manifest.mpd"
|
| 418 |
+
log_cb(" ✓ DASH OK")
|
| 419 |
+
|
| 420 |
+
# ── GITHUB UPLOAD FIX ──
|
| 421 |
+
log_cb(f" ☁ Subiendo a GitHub: {repo_name}")
|
| 422 |
+
headers = {"Authorization": f"token {token}"}
|
| 423 |
+
|
| 424 |
+
# 1. Crear repo o obtener URL
|
| 425 |
+
rr = requests.post("https://api.github.com/user/repos", headers=headers,
|
| 426 |
+
json={"name": repo_name, "private": True}, timeout=30)
|
| 427 |
+
|
| 428 |
+
if rr.status_code in [200, 201]:
|
| 429 |
+
html_url = rr.json()['html_url']
|
| 430 |
+
elif rr.status_code == 422:
|
| 431 |
+
# Ya existe, obtener info
|
| 432 |
+
u = requests.get("https://api.github.com/user", headers=headers, timeout=30)
|
| 433 |
+
uname = u.json()['login']
|
| 434 |
+
html_url = f"https://github.com/{uname}/{repo_name}"
|
| 435 |
+
log_cb(" ℹ Repositorio existente, actualizando...")
|
| 436 |
+
else:
|
| 437 |
+
raise Exception(f"GitHub API Error {rr.status_code}: {rr.text[:200]}")
|
| 438 |
+
|
| 439 |
+
git_url = html_url.replace('https://', f'https://{token}@') + '.git'
|
| 440 |
+
gd = str(out_dir)
|
| 441 |
+
|
| 442 |
+
# 2. Init y Config (Solución al error 128)
|
| 443 |
+
run_cmd(['git','init'], cwd=gd)
|
| 444 |
+
run_cmd(['git','config','user.email','converter@bot.com'], cwd=gd) # Fix identidad
|
| 445 |
+
run_cmd(['git','config','user.name','ConverterBot'], cwd=gd) # Fix identidad
|
| 446 |
+
run_cmd(['git','checkout','-b','main'], cwd=gd)
|
| 447 |
+
|
| 448 |
+
# 3. Remote
|
| 449 |
+
# Eliminamos origin si existiera para limpiar
|
| 450 |
+
run_cmd(['git','remote','remove','origin'], cwd=gd, check=False)
|
| 451 |
+
run_cmd(['git','remote','add','origin',git_url], cwd=gd)
|
| 452 |
+
run_cmd(['git','config','http.postBuffer','524288000'], cwd=gd)
|
| 453 |
+
|
| 454 |
+
all_files = os.listdir(gd)
|
| 455 |
+
ext_s = '.m4s' if stream_fmt == "DASH (MPD)" else '.ts'
|
| 456 |
+
ext_m = '.mpd' if stream_fmt == "DASH (MPD)" else '.m3u8'
|
| 457 |
+
segs = [f for f in all_files if f.endswith(ext_s)]
|
| 458 |
+
mans = [f for f in all_files if f.endswith(ext_m)]
|
| 459 |
+
|
| 460 |
+
# 4. Commit Inicial
|
| 461 |
+
run_cmd(['git','add'] + mans, cwd=gd)
|
| 462 |
+
run_cmd(['git','commit','-m',f'init {folder_name}'], cwd=gd)
|
| 463 |
+
|
| 464 |
+
# 5. Push Forzado (Solución a historiales divergentes)
|
| 465 |
+
# Usamos --force para sobrescribir si ya existía y evitar errores
|
| 466 |
+
log_cb(" 📤 Subiendo manifiestos...")
|
| 467 |
+
run_cmd(['git','push','-u','origin','main','--force'], cwd=gd)
|
| 468 |
+
|
| 469 |
+
# 6. Subida por lotes
|
| 470 |
+
log_cb(f" 📦 {len(segs)} segmentos → batch {batch_size}")
|
| 471 |
+
for i3 in range(0, len(segs), batch_size):
|
| 472 |
+
batch = segs[i3:i3+batch_size]
|
| 473 |
+
run_cmd(['git','add']+batch, cwd=gd)
|
| 474 |
+
run_cmd(['git','commit','-m',f'segs {i3}-{i3+len(batch)}'], cwd=gd)
|
| 475 |
+
|
| 476 |
+
# Reintentos
|
| 477 |
+
success = False
|
| 478 |
+
for retry in range(3):
|
| 479 |
+
try:
|
| 480 |
+
run_cmd(['git','push','origin','main'], cwd=gd)
|
| 481 |
+
success = True
|
| 482 |
+
break
|
| 483 |
+
except:
|
| 484 |
+
time.sleep(2)
|
| 485 |
+
|
| 486 |
+
if success:
|
| 487 |
+
log_cb(f" 📤 {min(i3+batch_size,len(segs))}/{len(segs)}")
|
| 488 |
+
else:
|
| 489 |
+
log_cb(f" ⚠ Error batch {i3}")
|
| 490 |
+
|
| 491 |
+
gp = f"https://gooplay.xyz/gp/stream.php?repo={repo_name}&branch=main&file={manifest_file}"
|
| 492 |
+
gh = f"{html_url}/blob/main/{manifest_file}"
|
| 493 |
+
final_links.append(f"{folder_name}\nGooplay: {gp}\nGitHub: {gh}")
|
| 494 |
+
log_cb(f" ✓ Completado: {folder_name}")
|
| 495 |
+
|
| 496 |
+
if delete_local:
|
| 497 |
+
shutil.rmtree(out_dir, ignore_errors=True)
|
| 498 |
+
log_cb(" ✓ Archivos locales borrados")
|
| 499 |
+
else:
|
| 500 |
+
log_cb(f" ℹ Archivos en: {out_dir}")
|
| 501 |
+
|
| 502 |
+
result = "\n\n".join(final_links)
|
| 503 |
+
log_cb("\n🎉 TODOS COMPLETADOS\n" + "="*40 + "\n" + result)
|
| 504 |
+
done_cb(True, result)
|
| 505 |
+
|
| 506 |
+
except Exception as e:
|
| 507 |
+
import traceback
|
| 508 |
+
err_msg = str(e)
|
| 509 |
+
log_cb(f"\n✗ ERROR: {err_msg}\n{traceback.format_exc()}")
|
| 510 |
+
done_cb(False, err_msg)
|
| 511 |
+
|
| 512 |
+
|
| 513 |
+
# ═════════════════════════════════════��══════════════════════
|
| 514 |
+
# GUI (Sin cambios mayores, solo ajustes menores)
|
| 515 |
+
# ════════════════════════════════════════════════════════════
|
| 516 |
+
class AppHLS(tk.Tk):
|
| 517 |
+
def __init__(self):
|
| 518 |
+
super().__init__()
|
| 519 |
+
self.title("HLS/DASH Converter EXTREME")
|
| 520 |
+
self.geometry("1100x750")
|
| 521 |
+
self.minsize(900, 620)
|
| 522 |
+
self.configure(bg=BG)
|
| 523 |
+
self.resizable(True, True)
|
| 524 |
+
self._build()
|
| 525 |
+
self._center()
|
| 526 |
+
|
| 527 |
+
def _center(self):
|
| 528 |
+
self.update_idletasks()
|
| 529 |
+
w, h = self.winfo_width(), self.winfo_height()
|
| 530 |
+
sw, sh = self.winfo_screenwidth(), self.winfo_screenheight()
|
| 531 |
+
self.geometry(f"{w}x{h}+{(sw-w)//2}+{(sh-h)//2}")
|
| 532 |
+
|
| 533 |
+
def _style(self):
|
| 534 |
+
s = ttk.Style(self)
|
| 535 |
+
s.theme_use("clam")
|
| 536 |
+
s.configure(".", background=BG, foreground=FG, font=FM,
|
| 537 |
+
fieldbackground=BG2, borderwidth=0)
|
| 538 |
+
s.configure("TFrame", background=BG)
|
| 539 |
+
s.configure("TLabel", background=BG, foreground=FG)
|
| 540 |
+
s.configure("TEntry", fieldbackground=BG2, foreground=FG,
|
| 541 |
+
insertcolor=FG, borderwidth=1)
|
| 542 |
+
s.configure("TCombobox", fieldbackground=BG2, background=BG3,
|
| 543 |
+
foreground=FG, arrowcolor=FG2)
|
| 544 |
+
s.map("TCombobox", fieldbackground=[("readonly", BG2)],
|
| 545 |
+
foreground=[("readonly", FG)])
|
| 546 |
+
s.configure("TCheckbutton", background=BG, foreground=FG2)
|
| 547 |
+
s.map("TCheckbutton", background=[("active", BG)])
|
| 548 |
+
s.configure("TRadiobutton", background=BG, foreground=FG2)
|
| 549 |
+
s.map("TRadiobutton", background=[("active", BG)])
|
| 550 |
+
s.configure("Horizontal.TProgressbar",
|
| 551 |
+
troughcolor=BG3, background=ACCENT,
|
| 552 |
+
borderwidth=0, thickness=6)
|
| 553 |
+
|
| 554 |
+
def _build(self):
|
| 555 |
+
self._style()
|
| 556 |
+
|
| 557 |
+
# HEADER
|
| 558 |
+
hdr = tk.Frame(self, bg=BG, pady=8)
|
| 559 |
+
hdr.pack(fill="x", padx=20)
|
| 560 |
+
tk.Label(hdr, text="HLS/DASH CONVERTER EXTREME", bg=BG, fg=ACCENT,
|
| 561 |
+
font=("Consolas", 15, "bold")).pack(side="left")
|
| 562 |
+
tk.Label(hdr, text=f" · {N_THREADS} threads · {RAMDISK}",
|
| 563 |
+
bg=BG, fg=FG3, font=FS).pack(side="left")
|
| 564 |
+
self._lbl_status = tk.Label(hdr, text="● Listo", bg=BG, fg=GREEN, font=FS)
|
| 565 |
+
self._lbl_status.pack(side="right")
|
| 566 |
+
|
| 567 |
+
tk.Frame(self, bg=BORDER, height=1).pack(fill="x", padx=20)
|
| 568 |
+
|
| 569 |
+
body = tk.Frame(self, bg=BG)
|
| 570 |
+
body.pack(fill="both", expand=True, padx=20, pady=8)
|
| 571 |
+
|
| 572 |
+
# ─ LEFT ─
|
| 573 |
+
left = tk.Frame(body, bg=BG, width=340)
|
| 574 |
+
left.pack(side="left", fill="y", padx=(0, 10))
|
| 575 |
+
left.pack_propagate(False)
|
| 576 |
+
|
| 577 |
+
self._section(left, "GITHUB TOKEN")
|
| 578 |
+
self._gh_token = self._entry_var(left, "ghp_…", show="*")
|
| 579 |
+
|
| 580 |
+
self._section(left, "FUENTES")
|
| 581 |
+
f_files = tk.Frame(left, bg=BG)
|
| 582 |
+
f_files.pack(fill="x", pady=(0, 4))
|
| 583 |
+
self._file_lbl = tk.Label(f_files, text="Sin archivos", bg=BG3, fg=FG3,
|
| 584 |
+
font=FS, anchor="w", padx=6, pady=3)
|
| 585 |
+
self._file_lbl.pack(side="left", fill="x", expand=True)
|
| 586 |
+
tk.Button(f_files, text="📁 Archivos", bg=BG3, fg=FG2, bd=0,
|
| 587 |
+
font=FS, cursor="hand2", command=self._browse_files,
|
| 588 |
+
padx=6, pady=3).pack(side="right")
|
| 589 |
+
self._files_list = []
|
| 590 |
+
|
| 591 |
+
tk.Label(left, text="URLs (una por línea):", bg=BG, fg=FG2, font=FS).pack(anchor="w")
|
| 592 |
+
self._urls_txt = tk.Text(left, bg=BG2, fg=FG, insertbackground=FG,
|
| 593 |
+
font=FS, bd=0, relief="flat", height=5, wrap="none")
|
| 594 |
+
self._urls_txt.pack(fill="x", pady=(0, 6))
|
| 595 |
+
|
| 596 |
+
self._section(left, "CONVERSIÓN")
|
| 597 |
+
self._conv_opt = ttk.Combobox(left, state="readonly", font=FS, height=6,
|
| 598 |
+
values=["Opción 1: Copy Video",
|
| 599 |
+
"Opción 2: Copy Video MP3",
|
| 600 |
+
"Opción 3: 1080p+720p H.264",
|
| 601 |
+
"Opción 4: 4K+1080p+720p H.264"])
|
| 602 |
+
self._conv_opt.set("Opción 1: Copy Video")
|
| 603 |
+
self._conv_opt.pack(fill="x", pady=(0, 4))
|
| 604 |
+
|
| 605 |
+
self._stream_fmt = ttk.Combobox(left, state="readonly", font=FS,
|
| 606 |
+
values=["HLS (M3U8)", "DASH (MPD)"])
|
| 607 |
+
self._stream_fmt.set("HLS (M3U8)")
|
| 608 |
+
self._stream_fmt.pack(fill="x", pady=(0, 6))
|
| 609 |
+
|
| 610 |
+
f_nums = tk.Frame(left, bg=BG)
|
| 611 |
+
f_nums.pack(fill="x", pady=(0, 4))
|
| 612 |
+
tk.Label(f_nums, text="Batch git:", bg=BG, fg=FG2, font=FS).pack(side="left")
|
| 613 |
+
self._batch_var = tk.StringVar(value="30")
|
| 614 |
+
tk.Entry(f_nums, textvariable=self._batch_var, bg=BG2, fg=FG,
|
| 615 |
+
insertbackground=FG, font=FS, bd=0, width=5,
|
| 616 |
+
relief="flat").pack(side="left", padx=4)
|
| 617 |
+
tk.Label(f_nums, text="Workers:", bg=BG, fg=FG2, font=FS).pack(side="left")
|
| 618 |
+
self._workers_var = tk.StringVar(value="4")
|
| 619 |
+
tk.Entry(f_nums, textvariable=self._workers_var, bg=BG2, fg=FG,
|
| 620 |
+
insertbackground=FG, font=FS, bd=0, width=4,
|
| 621 |
+
relief="flat").pack(side="left", padx=4)
|
| 622 |
+
|
| 623 |
+
self._del_local = tk.BooleanVar(value=True)
|
| 624 |
+
ttk.Checkbutton(left, text="Borrar archivos locales al terminar",
|
| 625 |
+
variable=self._del_local).pack(anchor="w")
|
| 626 |
+
|
| 627 |
+
self._section(left, "CONTENIDO")
|
| 628 |
+
self._ctype = tk.StringVar(value="Serie")
|
| 629 |
+
f_ct = tk.Frame(left, bg=BG)
|
| 630 |
+
f_ct.pack(fill="x")
|
| 631 |
+
for ct in ["Película", "Serie"]:
|
| 632 |
+
ttk.Radiobutton(f_ct, text=ct, variable=self._ctype,
|
| 633 |
+
value=ct, command=self._toggle_ctype).pack(side="left", padx=4)
|
| 634 |
+
|
| 635 |
+
self._movie_frame = tk.Frame(left, bg=BG)
|
| 636 |
+
tk.Label(self._movie_frame, text="Nombre:", bg=BG, fg=FG2, font=FS).pack(anchor="w")
|
| 637 |
+
self._mname = self._entry_var(self._movie_frame, "El Padrino")
|
| 638 |
+
|
| 639 |
+
self._serie_frame = tk.Frame(left, bg=BG)
|
| 640 |
+
tk.Label(self._serie_frame, text="Serie:", bg=BG, fg=FG2, font=FS).pack(anchor="w")
|
| 641 |
+
self._sname = self._entry_var(self._serie_frame, "Breaking Bad")
|
| 642 |
+
f_ss = tk.Frame(self._serie_frame, bg=BG)
|
| 643 |
+
f_ss.pack(fill="x")
|
| 644 |
+
tk.Label(f_ss, text="Temp:", bg=BG, fg=FG2, font=FS).pack(side="left")
|
| 645 |
+
self._season = tk.StringVar(value="1")
|
| 646 |
+
tk.Entry(f_ss, textvariable=self._season, bg=BG2, fg=FG,
|
| 647 |
+
insertbackground=FG, font=FS, bd=0, width=4,
|
| 648 |
+
relief="flat").pack(side="left", padx=4)
|
| 649 |
+
tk.Label(f_ss, text="Ep inicial:", bg=BG, fg=FG2, font=FS).pack(side="left")
|
| 650 |
+
self._ep_start = tk.StringVar(value="1")
|
| 651 |
+
tk.Entry(f_ss, textvariable=self._ep_start, bg=BG2, fg=FG,
|
| 652 |
+
insertbackground=FG, font=FS, bd=0, width=4,
|
| 653 |
+
relief="flat").pack(side="left", padx=4)
|
| 654 |
+
self._serie_frame.pack(fill="x", pady=(4, 0))
|
| 655 |
+
|
| 656 |
+
# ─ RIGHT ─
|
| 657 |
+
right = tk.Frame(body, bg=BG)
|
| 658 |
+
right.pack(side="right", fill="both", expand=True)
|
| 659 |
+
|
| 660 |
+
self._lbl_prog = tk.Label(right, text="", bg=BG, fg=FG2, font=FS, anchor="w")
|
| 661 |
+
self._lbl_prog.pack(fill="x")
|
| 662 |
+
self._pbar = ttk.Progressbar(right, mode="determinate",
|
| 663 |
+
style="Horizontal.TProgressbar")
|
| 664 |
+
self._pbar.pack(fill="x", pady=(2, 6))
|
| 665 |
+
|
| 666 |
+
tk.Label(right, text="LOG DE PROCESAMIENTO", bg=BG, fg=FG3,
|
| 667 |
+
font=("Consolas", 8, "bold")).pack(anchor="w")
|
| 668 |
+
log_f = tk.Frame(right, bg=BORDER)
|
| 669 |
+
log_f.pack(fill="both", expand=True, pady=(2, 6))
|
| 670 |
+
self._log_txt = tk.Text(
|
| 671 |
+
log_f, bg="#030810", fg=CYAN, font=("Consolas", 9),
|
| 672 |
+
bd=0, relief="flat", state="disabled", wrap="word",
|
| 673 |
+
insertbackground=FG, selectbackground=ACCENT2
|
| 674 |
+
)
|
| 675 |
+
sb = ttk.Scrollbar(log_f, command=self._log_txt.yview)
|
| 676 |
+
self._log_txt.configure(yscrollcommand=sb.set)
|
| 677 |
+
sb.pack(side="right", fill="y")
|
| 678 |
+
self._log_txt.pack(fill="both", expand=True, padx=1, pady=1)
|
| 679 |
+
|
| 680 |
+
tk.Label(right, text="LINKS GENERADOS", bg=BG, fg=FG3,
|
| 681 |
+
font=("Consolas", 8, "bold")).pack(anchor="w")
|
| 682 |
+
lnk_f = tk.Frame(right, bg=BORDER)
|
| 683 |
+
lnk_f.pack(fill="x", pady=(2, 6))
|
| 684 |
+
self._links_txt = tk.Text(
|
| 685 |
+
lnk_f, bg="#020c08", fg=GREEN, font=("Consolas", 9),
|
| 686 |
+
bd=0, relief="flat", state="disabled", height=6,
|
| 687 |
+
insertbackground=FG
|
| 688 |
+
)
|
| 689 |
+
lsb = ttk.Scrollbar(lnk_f, command=self._links_txt.yview)
|
| 690 |
+
self._links_txt.configure(yscrollcommand=lsb.set)
|
| 691 |
+
lsb.pack(side="right", fill="y")
|
| 692 |
+
self._links_txt.pack(fill="x", padx=1, pady=1)
|
| 693 |
+
|
| 694 |
+
bf = tk.Frame(right, bg=BG)
|
| 695 |
+
bf.pack(fill="x")
|
| 696 |
+
self._btn_start = tk.Button(
|
| 697 |
+
bf, text="🚀 INICIAR CONVERSIÓN",
|
| 698 |
+
bg=ACCENT, fg="white", bd=0, padx=16, pady=10,
|
| 699 |
+
font=("Consolas", 12, "bold"), cursor="hand2",
|
| 700 |
+
command=self._do_start
|
| 701 |
+
)
|
| 702 |
+
self._btn_start.pack(side="left", fill="x", expand=True)
|
| 703 |
+
tk.Button(
|
| 704 |
+
bf, text="✕", bg=BG3, fg=RED, bd=0,
|
| 705 |
+
padx=14, pady=10, font=FB, cursor="hand2",
|
| 706 |
+
command=self._do_cancel
|
| 707 |
+
).pack(side="right", padx=(6, 0))
|
| 708 |
+
|
| 709 |
+
self._cancelled = False
|
| 710 |
+
|
| 711 |
+
def _section(self, parent, text):
|
| 712 |
+
f = tk.Frame(parent, bg=BG)
|
| 713 |
+
f.pack(fill="x", pady=(10, 2))
|
| 714 |
+
tk.Label(f, text=text, bg=BG, fg=FG3,
|
| 715 |
+
font=("Consolas", 8, "bold")).pack(side="left")
|
| 716 |
+
tk.Frame(f, bg=BORDER, height=1).pack(side="right", fill="x",
|
| 717 |
+
expand=True, padx=(6, 0), pady=4)
|
| 718 |
+
|
| 719 |
+
def _entry_var(self, parent, placeholder, show=None):
|
| 720 |
+
v = tk.StringVar()
|
| 721 |
+
kw = {"show": show} if show else {}
|
| 722 |
+
tk.Entry(parent, textvariable=v, bg=BG2, fg=FG2,
|
| 723 |
+
insertbackground=FG, font=FS, bd=0, relief="flat",
|
| 724 |
+
**kw).pack(fill="x", ipady=5, pady=(0, 4))
|
| 725 |
+
return v
|
| 726 |
+
|
| 727 |
+
def _toggle_ctype(self):
|
| 728 |
+
if self._ctype.get() == "Película":
|
| 729 |
+
self._serie_frame.pack_forget()
|
| 730 |
+
self._movie_frame.pack(fill="x", pady=(4, 0))
|
| 731 |
+
else:
|
| 732 |
+
self._movie_frame.pack_forget()
|
| 733 |
+
self._serie_frame.pack(fill="x", pady=(4, 0))
|
| 734 |
+
|
| 735 |
+
def _browse_files(self):
|
| 736 |
+
files = filedialog.askopenfilenames(
|
| 737 |
+
filetypes=[("Video", "*.mp4 *.mkv *.avi *.mov *.ts *.m2ts *.webm"),
|
| 738 |
+
("Todos", "*.*")]
|
| 739 |
+
)
|
| 740 |
+
if files:
|
| 741 |
+
self._files_list = list(files)
|
| 742 |
+
names = ", ".join(Path(f).name for f in files)
|
| 743 |
+
self._file_lbl.configure(
|
| 744 |
+
text=names[:60] + ("…" if len(names) > 60 else ""), fg=FG2
|
| 745 |
+
)
|
| 746 |
+
|
| 747 |
+
def _log(self, msg):
|
| 748 |
+
self._log_txt.configure(state="normal")
|
| 749 |
+
self._log_txt.insert("end", msg + "\n")
|
| 750 |
+
self._log_txt.see("end")
|
| 751 |
+
self._log_txt.configure(state="disabled")
|
| 752 |
+
|
| 753 |
+
def _set_progress(self, pct, label=""):
|
| 754 |
+
self._pbar["value"] = pct
|
| 755 |
+
self._lbl_prog.configure(text=label)
|
| 756 |
+
self.update_idletasks()
|
| 757 |
+
|
| 758 |
+
def _set_links(self, text):
|
| 759 |
+
self._links_txt.configure(state="normal")
|
| 760 |
+
self._links_txt.delete("1.0", "end")
|
| 761 |
+
self._links_txt.insert("1.0", text)
|
| 762 |
+
self._links_txt.configure(state="disabled")
|
| 763 |
+
|
| 764 |
+
def _do_start(self):
|
| 765 |
+
token = self._gh_token.get().strip()
|
| 766 |
+
if not token:
|
| 767 |
+
messagebox.showwarning("Token", "Ingresá tu token de GitHub")
|
| 768 |
+
return
|
| 769 |
+
|
| 770 |
+
sources = []
|
| 771 |
+
for f in self._files_list:
|
| 772 |
+
sources.append({"type": "file", "value": f})
|
| 773 |
+
for line in self._urls_txt.get("1.0", "end").strip().split("\n"):
|
| 774 |
+
u = line.strip()
|
| 775 |
+
if u: sources.append({"type": "url", "value": u})
|
| 776 |
+
|
| 777 |
+
if not sources:
|
| 778 |
+
messagebox.showwarning("Fuente", "Agregá archivos o URLs")
|
| 779 |
+
return
|
| 780 |
+
|
| 781 |
+
self._btn_start.configure(state="disabled", bg=FG3)
|
| 782 |
+
self._lbl_status.configure(text="● Procesando…", fg=YELLOW)
|
| 783 |
+
self._log_txt.configure(state="normal")
|
| 784 |
+
self._log_txt.delete("1.0", "end")
|
| 785 |
+
self._log_txt.configure(state="disabled")
|
| 786 |
+
self._pbar["value"] = 0
|
| 787 |
+
self._cancelled = False
|
| 788 |
+
|
| 789 |
+
try:
|
| 790 |
+
batch_size = int(self._batch_var.get() or 30)
|
| 791 |
+
max_workers = int(self._workers_var.get() or 4)
|
| 792 |
+
season = int(self._season.get() or 1)
|
| 793 |
+
ep_start = int(self._ep_start.get() or 1)
|
| 794 |
+
except:
|
| 795 |
+
batch_size, max_workers, season, ep_start = 30, 4, 1, 1
|
| 796 |
+
|
| 797 |
+
job = {
|
| 798 |
+
'token': token, 'sources': sources,
|
| 799 |
+
'conv_opt': self._conv_opt.get(),
|
| 800 |
+
'stream_fmt': self._stream_fmt.get(),
|
| 801 |
+
'batch_size': batch_size,
|
| 802 |
+
'delete_local': self._del_local.get(),
|
| 803 |
+
'max_workers': max_workers,
|
| 804 |
+
'ctype': self._ctype.get(),
|
| 805 |
+
'mname': self._mname.get().strip(),
|
| 806 |
+
'sname': self._sname.get().strip(),
|
| 807 |
+
'season': season, 'ep_start': ep_start,
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
def _done(ok, result):
|
| 811 |
+
self.after(0, self._btn_start.configure, {"state": "normal", "bg": ACCENT})
|
| 812 |
+
if ok:
|
| 813 |
+
self.after(0, self._lbl_status.configure, {"text": "● Completado", "fg": GREEN})
|
| 814 |
+
self.after(0, self._set_links, result)
|
| 815 |
+
self.after(0, self._set_progress, 100, "Completado")
|
| 816 |
+
else:
|
| 817 |
+
self.after(0, self._lbl_status.configure, {"text": "● Error", "fg": RED})
|
| 818 |
+
|
| 819 |
+
threading.Thread(
|
| 820 |
+
target=run_conversion,
|
| 821 |
+
args=(job,
|
| 822 |
+
lambda m: self.after(0, self._log, m),
|
| 823 |
+
lambda p, l: self.after(0, self._set_progress, p, l),
|
| 824 |
+
_done),
|
| 825 |
+
daemon=True
|
| 826 |
+
).start()
|
| 827 |
+
|
| 828 |
+
def _do_cancel(self):
|
| 829 |
+
self._cancelled = True
|
| 830 |
+
self._lbl_status.configure(text="● Cancelado", fg=RED)
|
| 831 |
+
self._log("⛔ Cancelado por usuario")
|
| 832 |
+
self._btn_start.configure(state="normal", bg=ACCENT)
|
| 833 |
+
|
| 834 |
+
|
| 835 |
+
if __name__ == "__main__":
|
| 836 |
+
app = AppHLS()
|
| 837 |
+
app.mainloop()
|
hug.bat
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@echo off
|
| 2 |
+
title Video Converter Pro - Instalador
|
| 3 |
+
cls
|
| 4 |
+
|
| 5 |
+
echo ============================================
|
| 6 |
+
echo VIDEO CONVERTER PRO - INICIANDO
|
| 7 |
+
echo ============================================
|
| 8 |
+
echo.
|
| 9 |
+
|
| 10 |
+
:: 1. Verificar si Python está instalado
|
| 11 |
+
python --version >nul 2>&1
|
| 12 |
+
if %errorlevel% neq 0 (
|
| 13 |
+
echo [ERROR] Python no esta instalado o no esta en el PATH.
|
| 14 |
+
echo Por favor instala Python desde python.org
|
| 15 |
+
pause
|
| 16 |
+
exit /b
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
:: 2. Verificar e instalar HuggingFace Hub
|
| 20 |
+
echo [+] Verificando dependencias...
|
| 21 |
+
python -c "import huggingface_hub" >nul 2>&1
|
| 22 |
+
if %errorlevel% neq 0 (
|
| 23 |
+
echo.
|
| 24 |
+
echo [!] Libreria "huggingface_hub" no encontrada.
|
| 25 |
+
echo [>] Instalando modulo necesario para conectar con HuggingFace...
|
| 26 |
+
pip install huggingface_hub --quiet
|
| 27 |
+
if %errorlevel% neq 0 (
|
| 28 |
+
echo [ERROR] Hubo un problema al instalar las dependencias.
|
| 29 |
+
pause
|
| 30 |
+
exit /b
|
| 31 |
+
)
|
| 32 |
+
echo [OK] Dependencias instaladas correctamente.
|
| 33 |
+
) else (
|
| 34 |
+
echo [OK] Dependencias correctas.
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
:: 3. Verificar FFmpeg (Aviso legal, no se puede instalar automáticamente aquí)
|
| 38 |
+
ffmpeg -version >nul 2>&1
|
| 39 |
+
if %errorlevel% neq 0 (
|
| 40 |
+
echo.
|
| 41 |
+
echo [ADVERTENCIA] FFmpeg no se encontro en el sistema.
|
| 42 |
+
echo El programa podria abrir pero fallara al convertir videos.
|
| 43 |
+
echo Por favor instala FFmpeg y agregalo al PATH de Windows.
|
| 44 |
+
echo.
|
| 45 |
+
pause
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
:: 4. Ejecutar el Script Principal
|
| 49 |
+
echo.
|
| 50 |
+
echo [*] Iniciando aplicacion...
|
| 51 |
+
echo.
|
| 52 |
+
python "%~dp0converter.py"
|
| 53 |
+
|
| 54 |
+
pause
|
hug.py
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import os
|
| 3 |
+
import ctypes
|
| 4 |
+
import subprocess
|
| 5 |
+
import threading
|
| 6 |
+
import shutil
|
| 7 |
+
import re
|
| 8 |
+
import json
|
| 9 |
+
import tkinter as tk
|
| 10 |
+
from tkinter import ttk, filedialog, messagebox
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
# ─── AUTO-ELEVACIÓN ADMINISTRADOR ───────────────────────────
|
| 14 |
+
def is_admin():
|
| 15 |
+
try:
|
| 16 |
+
return ctypes.windll.shell32.IsUserAnAdmin()
|
| 17 |
+
except:
|
| 18 |
+
return False
|
| 19 |
+
|
| 20 |
+
def elevate():
|
| 21 |
+
if not is_admin():
|
| 22 |
+
ctypes.windll.shell32.ShellExecuteW(
|
| 23 |
+
None, "runas", sys.executable, " ".join(sys.argv), None, 1
|
| 24 |
+
)
|
| 25 |
+
sys.exit()
|
| 26 |
+
|
| 27 |
+
elevate()
|
| 28 |
+
|
| 29 |
+
# ─── VERIFICACIÓN DE LIBRERÍA HF ────────────────────────────
|
| 30 |
+
try:
|
| 31 |
+
from huggingface_hub import HfApi, login
|
| 32 |
+
HF_OK = True
|
| 33 |
+
except ImportError:
|
| 34 |
+
HF_OK = False
|
| 35 |
+
# Mostramos error gráfico inmediato si falta la librería
|
| 36 |
+
root = tk.Tk()
|
| 37 |
+
root.withdraw()
|
| 38 |
+
messagebox.showerror(
|
| 39 |
+
"Error de Dependencia",
|
| 40 |
+
" Falta instalar la librería 'huggingface_hub'.\n\n"
|
| 41 |
+
"Abre tu terminal (CMD) y ejecuta:\n\n"
|
| 42 |
+
"pip install huggingface_hub\n\n"
|
| 43 |
+
"Luego reinicia la aplicación."
|
| 44 |
+
)
|
| 45 |
+
sys.exit(1)
|
| 46 |
+
|
| 47 |
+
# ─── ESTILOS Y COLORES ──────────────────────────────────────
|
| 48 |
+
BG = "#0f172a"
|
| 49 |
+
CARD_BG = "#1e293b"
|
| 50 |
+
ACCENT = "#3b82f6"
|
| 51 |
+
ACCENT_H = "#2563eb"
|
| 52 |
+
GREEN = "#22c55e"
|
| 53 |
+
RED = "#ef4444"
|
| 54 |
+
YELLOW = "#f59e0b"
|
| 55 |
+
FG = "#f1f5f9"
|
| 56 |
+
FG2 = "#94a3b8"
|
| 57 |
+
BORDER = "#334155"
|
| 58 |
+
FONT_MAIN = ("Segoe UI", 10)
|
| 59 |
+
FONT_BOLD = ("Segoe UI", 10, "bold")
|
| 60 |
+
FONT_TITLE= ("Segoe UI", 14, "bold")
|
| 61 |
+
|
| 62 |
+
# ─── FUNCIONES UTILITARIAS ──────────────────────────────────
|
| 63 |
+
def safe_name(s, maxlen=120):
|
| 64 |
+
return re.sub(r'[<>:"/\\|?*\x00-\x1f]', '_', s).strip()[:maxlen]
|
| 65 |
+
|
| 66 |
+
def fmt_dur(secs):
|
| 67 |
+
s = int(secs)
|
| 68 |
+
return f"{s//3600:02d}:{(s%3600)//60:02d}:{s%60:02d}"
|
| 69 |
+
|
| 70 |
+
def probe(source):
|
| 71 |
+
info = {"audio": [], "subs": [], "duration": 0.0, "title": ""}
|
| 72 |
+
try:
|
| 73 |
+
r = subprocess.run(
|
| 74 |
+
["ffprobe", "-v", "quiet", "-print_format", "json",
|
| 75 |
+
"-show_format", "-show_streams", source],
|
| 76 |
+
capture_output=True, text=True, timeout=90
|
| 77 |
+
)
|
| 78 |
+
if r.returncode != 0:
|
| 79 |
+
return info
|
| 80 |
+
data = json.loads(r.stdout)
|
| 81 |
+
tags = data.get("format", {}).get("tags", {})
|
| 82 |
+
info["title"] = (tags.get("title") or tags.get("TITLE") or "").strip()
|
| 83 |
+
info["duration"] = float(data.get("format", {}).get("duration", 0) or 0)
|
| 84 |
+
ai = si = 0
|
| 85 |
+
for s in data.get("streams", []):
|
| 86 |
+
t = s.get("tags", {})
|
| 87 |
+
ct = s.get("codec_type", "")
|
| 88 |
+
if ct == "audio":
|
| 89 |
+
info["audio"].append({
|
| 90 |
+
"idx": ai, "codec": s.get("codec_name", "?"),
|
| 91 |
+
"lang": t.get("language", "und"), "ch": s.get("channels", 2),
|
| 92 |
+
"title": t.get("title", ""),
|
| 93 |
+
})
|
| 94 |
+
ai += 1
|
| 95 |
+
elif ct == "subtitle":
|
| 96 |
+
info["subs"].append({
|
| 97 |
+
"idx": si, "codec": s.get("codec_name", "?"),
|
| 98 |
+
"lang": t.get("language", "und"), "title": t.get("title", ""),
|
| 99 |
+
"forced": s.get("disposition", {}).get("forced", 0) == 1,
|
| 100 |
+
})
|
| 101 |
+
si += 1
|
| 102 |
+
except Exception:
|
| 103 |
+
pass
|
| 104 |
+
return info
|
| 105 |
+
|
| 106 |
+
NET_ARGS = [
|
| 107 |
+
"-user_agent", "Mozilla/5.0",
|
| 108 |
+
"-headers", "Referer: https://google.com\r\n",
|
| 109 |
+
"-timeout", "180000000",
|
| 110 |
+
"-reconnect", "1", "-reconnect_streamed", "1",
|
| 111 |
+
"-reconnect_at_eof", "1", "-reconnect_delay_max", "30",
|
| 112 |
+
"-rw_timeout", "180000000", "-multiple_requests", "1",
|
| 113 |
+
]
|
| 114 |
+
|
| 115 |
+
def run_ffmpeg(cmd, total, log_cb, progress_cb, label):
|
| 116 |
+
try:
|
| 117 |
+
proc = subprocess.Popen(
|
| 118 |
+
cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
|
| 119 |
+
universal_newlines=True, bufsize=1
|
| 120 |
+
)
|
| 121 |
+
pat = re.compile(r"time=(\d+):(\d+):(\d+)\.(\d+)")
|
| 122 |
+
for line in proc.stderr:
|
| 123 |
+
m = pat.search(line)
|
| 124 |
+
if m:
|
| 125 |
+
h, mi, s, cs = map(int, m.groups())
|
| 126 |
+
cur = h*3600 + mi*60 + s + cs/100
|
| 127 |
+
pct = min(99, int(cur / max(total, 1) * 100))
|
| 128 |
+
progress_cb(pct, f"{label}: {pct}% [{fmt_dur(cur)} / {fmt_dur(total)}]")
|
| 129 |
+
proc.wait()
|
| 130 |
+
return proc.returncode == 0
|
| 131 |
+
except Exception as e:
|
| 132 |
+
log_cb(f" ✗ ffmpeg: {e}")
|
| 133 |
+
return False
|
| 134 |
+
|
| 135 |
+
def hf_upload(token, repo_id, folder_path, log_cb, progress_cb):
|
| 136 |
+
try:
|
| 137 |
+
api = HfApi()
|
| 138 |
+
# Verificar token primero
|
| 139 |
+
try:
|
| 140 |
+
user_info = api.whoami(token=token)
|
| 141 |
+
log_cb(f" ✓ Conectado como: {user_info['name']}")
|
| 142 |
+
except Exception as auth_err:
|
| 143 |
+
log_cb(f" ✗ Error de autenticación: Token inválido o sin permisos.")
|
| 144 |
+
return False
|
| 145 |
+
|
| 146 |
+
# Crear/Subir
|
| 147 |
+
api.create_repo(repo_id=repo_id, repo_type="model", private=True, exist_ok=True, token=token)
|
| 148 |
+
|
| 149 |
+
files = list(Path(folder_path).iterdir())
|
| 150 |
+
total_files = len(files)
|
| 151 |
+
for i, f in enumerate(files):
|
| 152 |
+
if f.is_file():
|
| 153 |
+
rpath = f"videos/{Path(folder_path).name}/{f.name}"
|
| 154 |
+
log_cb(f" ↑ {f.name}")
|
| 155 |
+
progress_cb(int((i/total_files)*100), f"Subiendo {i+1}/{total_files}")
|
| 156 |
+
api.upload_file(
|
| 157 |
+
path_or_fileobj=str(f),
|
| 158 |
+
path_in_repo=rpath,
|
| 159 |
+
repo_id=repo_id,
|
| 160 |
+
repo_type="model",
|
| 161 |
+
token=token
|
| 162 |
+
)
|
| 163 |
+
progress_cb(100, "Subida completa")
|
| 164 |
+
log_cb(f" ✓ Subido → {repo_id}")
|
| 165 |
+
return True
|
| 166 |
+
except Exception as e:
|
| 167 |
+
log_cb(f" ✗ Upload error: {e}")
|
| 168 |
+
return False
|
| 169 |
+
|
| 170 |
+
def process_video(token, repo_id, source, is_url, mode,
|
| 171 |
+
audio_idx, gen_single, extract_sub, sub_idx,
|
| 172 |
+
delete_local, log_cb, progress_cb, done_cb):
|
| 173 |
+
try:
|
| 174 |
+
extra = NET_ARGS if is_url else []
|
| 175 |
+
log_cb("⟳ Analizando fuente…")
|
| 176 |
+
info = probe(source)
|
| 177 |
+
dur = info["duration"] if info["duration"] > 0 else 1
|
| 178 |
+
log_cb(f" ✓ {fmt_dur(dur)} | {len(info['audio'])} audio | {len(info['subs'])} sub")
|
| 179 |
+
|
| 180 |
+
base = info["title"] if info["title"] else Path(source).stem or "video"
|
| 181 |
+
fname = safe_name(base)
|
| 182 |
+
|
| 183 |
+
script_dir = Path(sys.argv[0]).parent.resolve()
|
| 184 |
+
output_root = script_dir / "Videos_Procesados"
|
| 185 |
+
output_root.mkdir(exist_ok=True)
|
| 186 |
+
|
| 187 |
+
tmp = output_root / fname
|
| 188 |
+
tmp.mkdir(exist_ok=True)
|
| 189 |
+
|
| 190 |
+
out_mp4 = tmp / f"{fname}.mp4"
|
| 191 |
+
|
| 192 |
+
cmd = ["ffmpeg", "-y"] + extra + ["-i", source, "-map", "0:v:0"]
|
| 193 |
+
for i in range(len(info["audio"])):
|
| 194 |
+
cmd.extend(["-map", f"0:a:{i}"])
|
| 195 |
+
|
| 196 |
+
if mode == "Copy + MP3":
|
| 197 |
+
cmd += ["-c:v", "copy"]
|
| 198 |
+
for i in range(len(info["audio"])):
|
| 199 |
+
cmd += [f"-c:a:{i}", "libmp3lame", f"-b:a:{i}", "320k"]
|
| 200 |
+
elif mode == "Copy + FLAC":
|
| 201 |
+
cmd += ["-c:v", "copy"]
|
| 202 |
+
for i in range(len(info["audio"])):
|
| 203 |
+
cmd += [f"-c:a:{i}", "flac"]
|
| 204 |
+
else:
|
| 205 |
+
cmd += ["-c:v", "libx264", "-vf", "scale=-2:1080", "-preset", "fast", "-crf", "18"]
|
| 206 |
+
for i in range(len(info["audio"])):
|
| 207 |
+
cmd += [f"-c:a:{i}", "libmp3lame", f"-b:a:{i}", "320k"]
|
| 208 |
+
|
| 209 |
+
cmd += ["-map_metadata", "0", str(out_mp4)]
|
| 210 |
+
|
| 211 |
+
log_cb(f"⚙ Convirtiendo ({mode})…")
|
| 212 |
+
ok = run_ffmpeg(cmd, dur, log_cb, progress_cb, "Convirtiendo")
|
| 213 |
+
if not ok:
|
| 214 |
+
log_cb("✗ Conversión falló")
|
| 215 |
+
if delete_local:
|
| 216 |
+
shutil.rmtree(tmp, ignore_errors=True)
|
| 217 |
+
done_cb(False)
|
| 218 |
+
return
|
| 219 |
+
|
| 220 |
+
progress_cb(100, "Conversión OK")
|
| 221 |
+
log_cb(" ✓ Conversión OK")
|
| 222 |
+
|
| 223 |
+
if gen_single and info["audio"] and audio_idx < len(info["audio"]):
|
| 224 |
+
sp = tmp / f"{fname}_aud{audio_idx}.mp4"
|
| 225 |
+
sc = ["ffmpeg", "-y", "-i", str(out_mp4),
|
| 226 |
+
"-map", "0:v:0", "-map", f"0:a:{audio_idx}", "-c", "copy", str(sp)]
|
| 227 |
+
run_ffmpeg(sc, dur, log_cb, progress_cb, "Audio individual")
|
| 228 |
+
log_cb(" ✓ Audio individual extraído")
|
| 229 |
+
|
| 230 |
+
if extract_sub and info["subs"] and sub_idx < len(info["subs"]):
|
| 231 |
+
vtt = tmp / f"{fname}_sub{sub_idx}.vtt"
|
| 232 |
+
sc = ["ffmpeg", "-y"] + extra + [
|
| 233 |
+
"-i", source, "-map", f"0:s:{sub_idx}", "-c:s", "webvtt", str(vtt)]
|
| 234 |
+
try:
|
| 235 |
+
subprocess.run(sc, capture_output=True, check=True, timeout=120)
|
| 236 |
+
log_cb(" ✓ Subtítulo extraído")
|
| 237 |
+
except Exception as e:
|
| 238 |
+
log_cb(f" ⚠ sub error: {e}")
|
| 239 |
+
|
| 240 |
+
log_cb(f"☁ Subiendo a HuggingFace → {repo_id}")
|
| 241 |
+
ok2 = hf_upload(token, repo_id, str(tmp), log_cb, progress_cb)
|
| 242 |
+
|
| 243 |
+
if delete_local:
|
| 244 |
+
shutil.rmtree(tmp, ignore_errors=True)
|
| 245 |
+
log_cb(" ✓ Archivos locales borrados")
|
| 246 |
+
else:
|
| 247 |
+
log_cb(f" ℹ Archivos guardados en: {tmp}")
|
| 248 |
+
|
| 249 |
+
done_cb(ok2)
|
| 250 |
+
|
| 251 |
+
except Exception as e:
|
| 252 |
+
import traceback
|
| 253 |
+
log_cb(f"✗ Error: {e}\n{traceback.format_exc()}")
|
| 254 |
+
done_cb(False)
|
| 255 |
+
|
| 256 |
+
class App(tk.Tk):
|
| 257 |
+
def __init__(self):
|
| 258 |
+
super().__init__()
|
| 259 |
+
self.title("Video Converter Pro")
|
| 260 |
+
self.geometry("1000x720")
|
| 261 |
+
self.configure(bg=BG)
|
| 262 |
+
self.resizable(True, True)
|
| 263 |
+
self._repos = []
|
| 264 |
+
self._info = None
|
| 265 |
+
self._build()
|
| 266 |
+
|
| 267 |
+
def _build(self):
|
| 268 |
+
style = ttk.Style(self)
|
| 269 |
+
style.theme_use("clam")
|
| 270 |
+
style.configure(".", background=BG, foreground=FG, font=FONT_MAIN, borderwidth=0)
|
| 271 |
+
style.configure("TFrame", background=BG)
|
| 272 |
+
style.configure("Card.TFrame", background=CARD_BG)
|
| 273 |
+
style.configure("TLabel", background=BG, foreground=FG, font=FONT_MAIN)
|
| 274 |
+
style.configure("Card.TLabel", background=CARD_BG, foreground=FG, font=FONT_MAIN)
|
| 275 |
+
style.configure("Header.TLabel", background=BG, foreground=FG, font=FONT_TITLE)
|
| 276 |
+
style.configure("Dim.TLabel", background=BG, foreground=FG2, font=FONT_MAIN)
|
| 277 |
+
style.configure("Accent.TButton", background=ACCENT, foreground="white", font=FONT_BOLD, padding=10, borderwidth=0)
|
| 278 |
+
style.map("Accent.TButton", background=[("active", ACCENT_H)])
|
| 279 |
+
style.configure("TCombobox", fieldbackground=CARD_BG, background=BG, foreground=FG, arrowcolor=FG2, borderwidth=1)
|
| 280 |
+
style.map("TCombobox", fieldbackground=[("readonly", CARD_BG)], foreground=[("readonly", FG)])
|
| 281 |
+
style.configure("Horizontal.TProgressbar", troughcolor=BG, background=ACCENT, thickness=8)
|
| 282 |
+
|
| 283 |
+
main_container = tk.Frame(self, bg=BG)
|
| 284 |
+
main_container.pack(fill="both", expand=True, padx=20, pady=20)
|
| 285 |
+
|
| 286 |
+
header_frame = tk.Frame(main_container, bg=BG)
|
| 287 |
+
header_frame.pack(fill="x", pady=(0, 20))
|
| 288 |
+
tk.Label(header_frame, text="VIDEO CONVERTER PRO", bg=BG, fg=ACCENT, font=FONT_TITLE).pack(side="left")
|
| 289 |
+
self._lbl_status = tk.Label(header_frame, text="● Listo", bg=BG, fg=GREEN, font=FONT_MAIN)
|
| 290 |
+
self._lbl_status.pack(side="right")
|
| 291 |
+
|
| 292 |
+
body_grid = tk.Frame(main_container, bg=BG)
|
| 293 |
+
body_grid.pack(fill="both", expand=True)
|
| 294 |
+
body_grid.columnconfigure(0, weight=1, uniform="group1")
|
| 295 |
+
body_grid.columnconfigure(1, weight=1, uniform="group1")
|
| 296 |
+
body_grid.rowconfigure(0, weight=1)
|
| 297 |
+
|
| 298 |
+
left_panel = tk.Frame(body_grid, bg=CARD_BG, padx=15, pady=15)
|
| 299 |
+
left_panel.grid(row=0, column=0, sticky="nsew", padx=(0, 10))
|
| 300 |
+
|
| 301 |
+
self._build_hf_section(left_panel)
|
| 302 |
+
self._build_options_section(left_panel)
|
| 303 |
+
self._build_tracks_section(left_panel)
|
| 304 |
+
|
| 305 |
+
right_panel = tk.Frame(body_grid, bg=CARD_BG, padx=15, pady=15)
|
| 306 |
+
right_panel.grid(row=0, column=1, sticky="nsew", padx=(10, 0))
|
| 307 |
+
|
| 308 |
+
self._build_source_section(right_panel)
|
| 309 |
+
self._build_log_section(right_panel)
|
| 310 |
+
|
| 311 |
+
def _create_section(self, parent, title):
|
| 312 |
+
f = tk.Frame(parent, bg=CARD_BG, pady=5)
|
| 313 |
+
f.pack(fill="x", pady=(0, 10))
|
| 314 |
+
tk.Label(f, text=title.upper(), bg=CARD_BG, fg=FG2, font=("Segoe UI", 8, "bold")).pack(anchor="w")
|
| 315 |
+
sep = tk.Frame(f, bg=BORDER, height=1)
|
| 316 |
+
sep.pack(fill="x", pady=(5, 10))
|
| 317 |
+
return f
|
| 318 |
+
|
| 319 |
+
def _build_hf_section(self, parent):
|
| 320 |
+
s = self._create_section(parent, "HuggingFace")
|
| 321 |
+
tk.Label(s, text="Token:", bg=CARD_BG, fg=FG2).pack(anchor="w")
|
| 322 |
+
self._hf_token = tk.StringVar()
|
| 323 |
+
tk.Entry(s, textvariable=self._hf_token, show="*", bg=BG, fg=FG, insertbackground=FG, font=FONT_MAIN, relief="flat").pack(fill="x", ipady=4, pady=(0, 10))
|
| 324 |
+
|
| 325 |
+
f_repo = tk.Frame(s, bg=CARD_BG)
|
| 326 |
+
f_repo.pack(fill="x", pady=(0, 5))
|
| 327 |
+
tk.Label(f_repo, text="Repositorio:", bg=CARD_BG, fg=FG2).pack(anchor="w")
|
| 328 |
+
cb_frame = tk.Frame(f_repo, bg=CARD_BG)
|
| 329 |
+
cb_frame.pack(fill="x")
|
| 330 |
+
self._repo_cb = ttk.Combobox(cb_frame, state="readonly", font=FONT_MAIN)
|
| 331 |
+
self._repo_cb.pack(side="left", fill="x", expand=True)
|
| 332 |
+
tk.Button(cb_frame, text="↻", bg=BG, fg=ACCENT, bd=0, font=FONT_BOLD, cursor="hand2", command=self._load_repos).pack(side="right", padx=(5, 0))
|
| 333 |
+
|
| 334 |
+
def _build_options_section(self, parent):
|
| 335 |
+
s = self._create_section(parent, "Configuración")
|
| 336 |
+
self._mode = tk.StringVar(value="Copy + MP3")
|
| 337 |
+
for m in ["Copy + MP3", "Copy + FLAC", "H264 1080p"]:
|
| 338 |
+
ttk.Radiobutton(s, text=m, variable=self._mode, value=m).pack(anchor="w", pady=2)
|
| 339 |
+
|
| 340 |
+
self._gen_single = tk.BooleanVar(value=False)
|
| 341 |
+
self._ext_sub = tk.BooleanVar(value=False)
|
| 342 |
+
self._del_local = tk.BooleanVar(value=True)
|
| 343 |
+
|
| 344 |
+
opts_frame = tk.Frame(s, bg=CARD_BG)
|
| 345 |
+
opts_frame.pack(fill="x", pady=(10, 0))
|
| 346 |
+
ttk.Checkbutton(opts_frame, text="Audio individual", variable=self._gen_single).pack(anchor="w")
|
| 347 |
+
ttk.Checkbutton(opts_frame, text="Extraer subtítulos", variable=self._ext_sub).pack(anchor="w")
|
| 348 |
+
ttk.Checkbutton(opts_frame, text="Borrar locales al terminar", variable=self._del_local).pack(anchor="w")
|
| 349 |
+
|
| 350 |
+
def _build_tracks_section(self, parent):
|
| 351 |
+
s = self._create_section(parent, "Pistas Detectadas")
|
| 352 |
+
tk.Label(s, text="Pista Audio:", bg=CARD_BG, fg=FG2).pack(anchor="w")
|
| 353 |
+
self._aud_cb = ttk.Combobox(s, state="readonly", font=FONT_MAIN)
|
| 354 |
+
self._aud_cb.pack(fill="x", pady=(0, 8))
|
| 355 |
+
tk.Label(s, text="Pista Subtítulo:", bg=CARD_BG, fg=FG2).pack(anchor="w")
|
| 356 |
+
self._sub_cb = ttk.Combobox(s, state="readonly", font=FONT_MAIN)
|
| 357 |
+
self._sub_cb.pack(fill="x")
|
| 358 |
+
|
| 359 |
+
def _build_source_section(self, parent):
|
| 360 |
+
s = self._create_section(parent, "Fuente")
|
| 361 |
+
|
| 362 |
+
tabs = tk.Frame(s, bg=CARD_BG)
|
| 363 |
+
tabs.pack(fill="x", pady=(0, 10))
|
| 364 |
+
self._src_mode = tk.StringVar(value="file")
|
| 365 |
+
|
| 366 |
+
btn_file = tk.Button(tabs, text="Archivo", bg=ACCENT, fg="white", bd=0, padx=10, pady=5, font=FONT_MAIN, command=lambda: self._switch_src("file", btn_file, btn_url))
|
| 367 |
+
btn_file.pack(side="left")
|
| 368 |
+
btn_url = tk.Button(tabs, text="URL", bg=BG, fg=FG2, bd=0, padx=10, pady=5, font=FONT_MAIN, command=lambda: self._switch_src("url", btn_file, btn_url))
|
| 369 |
+
btn_url.pack(side="left", padx=5)
|
| 370 |
+
|
| 371 |
+
self._file_frame = tk.Frame(s, bg=CARD_BG)
|
| 372 |
+
self._file_frame.pack(fill="x")
|
| 373 |
+
self._file_var = tk.StringVar()
|
| 374 |
+
tk.Entry(self._file_frame, textvariable=self._file_var, bg=BG, fg=FG, insertbackground=FG, font=FONT_MAIN, relief="flat").pack(side="left", fill="x", expand=True, ipady=4, padx=(0, 5))
|
| 375 |
+
tk.Button(self._file_frame, text="Buscar", bg=BG, fg=ACCENT, bd=0, padx=8, pady=4, font=FONT_MAIN, command=self._browse).pack(side="right")
|
| 376 |
+
|
| 377 |
+
self._url_frame = tk.Frame(s, bg=CARD_BG)
|
| 378 |
+
self._url_var = tk.StringVar()
|
| 379 |
+
tk.Entry(self._url_frame, textvariable=self._url_var, bg=BG, fg=FG, insertbackground=FG, font=FONT_MAIN, relief="flat").pack(fill="x", ipady=4)
|
| 380 |
+
|
| 381 |
+
self._switch_src("file", btn_file, btn_url)
|
| 382 |
+
|
| 383 |
+
self._lbl_info = tk.Label(s, text="Sin análisis", bg=BG, fg=FG2, font=FONT_MAIN, anchor="w", padx=10, pady=5)
|
| 384 |
+
self._lbl_info.pack(fill="x", pady=(10, 0))
|
| 385 |
+
|
| 386 |
+
btn_frame = tk.Frame(s, bg=CARD_BG)
|
| 387 |
+
btn_frame.pack(fill="x", pady=(10, 0))
|
| 388 |
+
tk.Button(btn_frame, text="ANALIZAR", bg=BG, fg=ACCENT, bd=0, padx=10, pady=5, font=FONT_BOLD, cursor="hand2", command=self._do_analyze).pack(side="left")
|
| 389 |
+
self._btn_proc = tk.Button(btn_frame, text="PROCESAR Y SUBIR", bg=ACCENT, fg="white", bd=0, padx=10, pady=5, font=FONT_BOLD, cursor="hand2", command=self._do_process)
|
| 390 |
+
self._btn_proc.pack(side="right")
|
| 391 |
+
|
| 392 |
+
def _build_log_section(self, parent):
|
| 393 |
+
s = self._create_section(parent, "Progreso")
|
| 394 |
+
self._lbl_prog = tk.Label(s, text="Esperando...", bg=CARD_BG, fg=FG2, font=FONT_MAIN, anchor="w")
|
| 395 |
+
self._lbl_prog.pack(fill="x")
|
| 396 |
+
self._pbar = ttk.Progressbar(s, mode="determinate", style="Horizontal.TProgressbar")
|
| 397 |
+
self._pbar.pack(fill="x", pady=(5, 10))
|
| 398 |
+
|
| 399 |
+
log_f = tk.Frame(s, bg=BG, padx=1, pady=1)
|
| 400 |
+
log_f.pack(fill="both", expand=True)
|
| 401 |
+
self._log_txt = tk.Text(log_f, bg=BG, fg="#0ea5e9", font=("Consolas", 9), bd=0, relief="flat", state="disabled", wrap="word")
|
| 402 |
+
sb = ttk.Scrollbar(log_f, command=self._log_txt.yview)
|
| 403 |
+
self._log_txt.configure(yscrollcommand=sb.set)
|
| 404 |
+
sb.pack(side="right", fill="y")
|
| 405 |
+
self._log_txt.pack(fill="both", expand=True)
|
| 406 |
+
|
| 407 |
+
def _switch_src(self, mode, btn_file, btn_url):
|
| 408 |
+
self._src_mode.set(mode)
|
| 409 |
+
if mode == "file":
|
| 410 |
+
self._file_frame.pack(fill="x")
|
| 411 |
+
self._url_frame.pack_forget()
|
| 412 |
+
btn_file.configure(bg=ACCENT, fg="white")
|
| 413 |
+
btn_url.configure(bg=BG, fg=FG2)
|
| 414 |
+
else:
|
| 415 |
+
self._url_frame.pack(fill="x")
|
| 416 |
+
self._file_frame.pack_forget()
|
| 417 |
+
btn_url.configure(bg=ACCENT, fg="white")
|
| 418 |
+
btn_file.configure(bg=BG, fg=FG2)
|
| 419 |
+
|
| 420 |
+
def _browse(self):
|
| 421 |
+
p = filedialog.askopenfilename(filetypes=[("Video", "*.mp4 *.mkv *.avi *.mov *.ts"), ("Todos", "*.*")])
|
| 422 |
+
if p:
|
| 423 |
+
self._file_var.set(p)
|
| 424 |
+
|
| 425 |
+
def _log(self, msg):
|
| 426 |
+
self._log_txt.configure(state="normal")
|
| 427 |
+
self._log_txt.insert("end", msg + "\n")
|
| 428 |
+
self._log_txt.see("end")
|
| 429 |
+
self._log_txt.configure(state="disabled")
|
| 430 |
+
|
| 431 |
+
def _set_progress(self, pct, label=""):
|
| 432 |
+
self._pbar["value"] = pct
|
| 433 |
+
self._lbl_prog.configure(text=label)
|
| 434 |
+
self.update_idletasks()
|
| 435 |
+
|
| 436 |
+
def _get_source(self):
|
| 437 |
+
if self._src_mode.get() == "file":
|
| 438 |
+
return self._file_var.get().strip(), False
|
| 439 |
+
return self._url_var.get().strip(), True
|
| 440 |
+
|
| 441 |
+
def _load_repos(self):
|
| 442 |
+
token = self._hf_token.get().strip()
|
| 443 |
+
if not token:
|
| 444 |
+
messagebox.showwarning("Error", "Por favor ingresa un Token primero.")
|
| 445 |
+
return
|
| 446 |
+
|
| 447 |
+
self._log("⟳ Cargando repositorios...")
|
| 448 |
+
|
| 449 |
+
def _go():
|
| 450 |
+
try:
|
| 451 |
+
api = HfApi()
|
| 452 |
+
# Verificar credenciales y obtener usuario
|
| 453 |
+
user_info = api.whoami(token=token)
|
| 454 |
+
name = user_info.get('name', '')
|
| 455 |
+
|
| 456 |
+
# Listar modelos
|
| 457 |
+
models = api.list_models(author=name, token=token, limit=100)
|
| 458 |
+
repos = [m.modelId for m in models]
|
| 459 |
+
|
| 460 |
+
self._repos = repos
|
| 461 |
+
self._repo_cb["values"] = repos
|
| 462 |
+
if repos:
|
| 463 |
+
self._repo_cb.set(repos[0])
|
| 464 |
+
self._log(f"✓ {len(repos)} repositorios encontrados para '{name}'.")
|
| 465 |
+
except Exception as e:
|
| 466 |
+
self._log(f"✗ Error: {e}")
|
| 467 |
+
# Si falla, intentar crear repo nuevo manualmente en el combobox
|
| 468 |
+
self.after(0, lambda: messagebox.showerror("Error HF", f"No se pudieron cargar repos.\nVerifica que el token tenga permisos de 'Write'.\n\nError: {e}"))
|
| 469 |
+
|
| 470 |
+
threading.Thread(target=_go, daemon=True).start()
|
| 471 |
+
|
| 472 |
+
def _do_analyze(self):
|
| 473 |
+
source, _ = self._get_source()
|
| 474 |
+
if not source:
|
| 475 |
+
messagebox.showwarning("Error", "Selecciona una fuente.")
|
| 476 |
+
return
|
| 477 |
+
def _go():
|
| 478 |
+
self._log("⟳ Analizando...")
|
| 479 |
+
info = probe(source)
|
| 480 |
+
self._info = info
|
| 481 |
+
ac = [f"[{t['idx']}] {t['lang']} · {t['codec']}" for t in info["audio"]]
|
| 482 |
+
sc = [f"[{t['idx']}] {t['lang']} · {t['codec']}" for t in info["subs"]]
|
| 483 |
+
self._aud_cb["values"] = ac
|
| 484 |
+
self._sub_cb["values"] = sc
|
| 485 |
+
if ac: self._aud_cb.set(ac[0])
|
| 486 |
+
if sc: self._sub_cb.set(sc[0])
|
| 487 |
+
txt = f"✓ {info['title'] or 'Video'} | {fmt_dur(info['duration'])} | {len(ac)} aud | {len(sc)} sub"
|
| 488 |
+
self._lbl_info.configure(text=txt, fg=GREEN)
|
| 489 |
+
self._log(txt)
|
| 490 |
+
threading.Thread(target=_go, daemon=True).start()
|
| 491 |
+
|
| 492 |
+
def _do_process(self):
|
| 493 |
+
token = self._hf_token.get().strip()
|
| 494 |
+
repo_id = self._repo_cb.get().strip()
|
| 495 |
+
source, is_url = self._get_source()
|
| 496 |
+
|
| 497 |
+
if not token: return messagebox.showwarning("Error", "Ingresa el Token.")
|
| 498 |
+
if not repo_id: return messagebox.showwarning("Error", "Selecciona o escribe un Repositorio.")
|
| 499 |
+
if not source: return messagebox.showwarning("Error", "Selecciona una Fuente.")
|
| 500 |
+
|
| 501 |
+
ai = 0
|
| 502 |
+
si = 0
|
| 503 |
+
try:
|
| 504 |
+
if self._aud_cb.get(): ai = int(self._aud_cb.get().split("]")[0].strip("["))
|
| 505 |
+
except: pass
|
| 506 |
+
try:
|
| 507 |
+
if self._sub_cb.get(): si = int(self._sub_cb.get().split("]")[0].strip("["))
|
| 508 |
+
except: pass
|
| 509 |
+
|
| 510 |
+
self._btn_proc.configure(state="disabled", bg=FG2)
|
| 511 |
+
self._lbl_status.configure(text="● Procesando…", fg=YELLOW)
|
| 512 |
+
self._log_txt.configure(state="normal")
|
| 513 |
+
self._log_txt.delete("1.0", "end")
|
| 514 |
+
self._log_txt.configure(state="disabled")
|
| 515 |
+
self._pbar["value"] = 0
|
| 516 |
+
|
| 517 |
+
def _done(ok):
|
| 518 |
+
self._btn_proc.configure(state="normal", bg=ACCENT)
|
| 519 |
+
if ok:
|
| 520 |
+
self._lbl_status.configure(text="● Completado", fg=GREEN)
|
| 521 |
+
self._log("✓ COMPLETADO")
|
| 522 |
+
self._set_progress(100, "Completado")
|
| 523 |
+
else:
|
| 524 |
+
self._lbl_status.configure(text="● Error", fg=RED)
|
| 525 |
+
self._log("✗ FALLÓ")
|
| 526 |
+
|
| 527 |
+
threading.Thread(
|
| 528 |
+
target=process_video,
|
| 529 |
+
args=(token, repo_id, source, is_url, self._mode.get(),
|
| 530 |
+
ai, self._gen_single.get(), self._ext_sub.get(), si,
|
| 531 |
+
self._del_local.get(),
|
| 532 |
+
lambda m: self.after(0, self._log, m),
|
| 533 |
+
lambda p, l: self.after(0, self._set_progress, p, l),
|
| 534 |
+
lambda ok: self.after(0, _done, ok)),
|
| 535 |
+
daemon=True
|
| 536 |
+
).start()
|
| 537 |
+
|
| 538 |
+
if __name__ == "__main__":
|
| 539 |
+
app = App()
|
| 540 |
+
app.mainloop()
|