CineMax commited on
Commit
dac0b2e
·
verified ·
1 Parent(s): 6df2075

Upload 3 files

Browse files
Files changed (3) hide show
  1. hls.py.txt +837 -0
  2. hug.bat +54 -0
  3. 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()