pll / c.py
CineMax's picture
Upload 11 files
182cdfb verified
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import subprocess
import os
import threading
import json
from pathlib import Path
import re
from urllib.parse import urlparse, unquote
import platform
import shutil
import stat
try:
from tkinterdnd2 import DND_FILES, TkinterDnD
DRAG_DROP_AVAILABLE = True
except ImportError:
DRAG_DROP_AVAILABLE = False
TkinterDnD = tk
try:
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
class HLSConverterApp:
def __init__(self, root):
self.root = root
self.root.title("HLS Converter Pro + GitHub Uploader")
self.root.geometry("1100x900")
self.root.configure(bg="#121212")
# Variables principales
self.video_file = tk.StringVar()
self.video_url = tk.StringVar()
self.input_type = tk.StringVar(value="file")
self.conversion_mode = tk.StringVar(value="copy_video")
self.audio_bitrate = tk.StringVar(value="192k")
self.repo_url = tk.StringVar()
self.github_token = tk.StringVar()
self.video_batch_size = tk.IntVar(value=50)
self.audio_batch_size = tk.IntVar(value=50)
self.mode = tk.StringVar(value="normal")
self.delete_local = tk.BooleanVar(value=True)
self.bulk_sources = []
self.output_dir = ""
self.last_output_dir = ""
self.last_direct_link = ""
self.processing = False
# Progreso
self.progress_popup = None
self.step_var = None
self.percent_var = None
self.progress_var = None
self.base_progress = 0.0
self.progress_span = 100.0
self.subprocess_flags = subprocess.CREATE_NO_WINDOW if platform.system() == "Windows" else 0
# Idiomas
self.lang_names = {
'und': 'Principal',
'eng': 'English', 'en': 'English',
'spa': 'Español', 'es': 'Español',
'fra': 'Français', 'fr': 'Français',
'deu': 'Deutsch', 'de': 'Deutsch',
'ita': 'Italiano', 'it': 'Italiano',
'por': 'Português', 'pt': 'Português',
'rus': 'Русский', 'ru': 'Русский',
'jpn': '日本語', 'ja': '日本語',
'kor': '한국어', 'ko': '한국어',
'zho': '中文', 'zh': '中文',
'ara': 'العربية', 'ar': 'العربية'
}
self.setup_theme()
self.create_widgets()
self.github_token.set("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")
def setup_theme(self):
style = ttk.Style()
style.theme_use('clam')
style.configure("TFrame", background="#121212")
style.configure("Card.TFrame", background="#1e1e1e")
style.configure("TLabel", background="#121212", foreground="#e0e0e0", font=("Segoe UI", 10))
style.configure("Title.TLabel", font=("Segoe UI", 18, "bold"), foreground="#00d4ff")
style.configure("TButton", font=("Segoe UI", 10, "bold"))
style.configure("Accent.TButton", background="#00d4ff", foreground="#000000")
style.configure("TEntry", fieldbackground="#2d2d2d", foreground="#e0e0e0")
style.configure("TRadiobutton", background="#121212", foreground="#e0e0e0")
style.configure("Custom.Horizontal.TProgressbar", thickness=20, background="#00d4ff")
def create_widgets(self):
main_frame = ttk.Frame(self.root, padding="20")
main_frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(main_frame, text="🎬 HLS Converter Pro 2026", style="Title.TLabel").pack(pady=(0, 20))
self.notebook = ttk.Notebook(main_frame)
self.notebook.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
# Tab General
tab_general = ttk.Frame(self.notebook, padding="10")
self.notebook.add(tab_general, text="General")
token_frame = ttk.LabelFrame(tab_general, text="GitHub Token", padding="15", style="Card.TFrame")
token_frame.pack(fill=tk.X, pady=(0, 15))
ttk.Entry(token_frame, textvariable=self.github_token, width=70, show="*").pack(fill=tk.X)
mode_frame = ttk.LabelFrame(tab_general, text="Modo", padding="15", style="Card.TFrame")
mode_frame.pack(fill=tk.X, pady=(0, 15))
ttk.Radiobutton(mode_frame, text="Normal (un video, repo existente)", variable=self.mode, value="normal", command=self.toggle_mode).pack(anchor=tk.W, pady=3)
ttk.Radiobutton(mode_frame, text="Bulk (múltiples videos, crea repos automáticamente)", variable=self.mode, value="bulk", command=self.toggle_mode).pack(anchor=tk.W, pady=3)
conv_frame = ttk.LabelFrame(tab_general, text="Conversión", padding="15", style="Card.TFrame")
conv_frame.pack(fill=tk.X, pady=(0, 15))
ttk.Radiobutton(conv_frame, text="⚡ Copy Video + Copy Audio (más rápido)", variable=self.conversion_mode, value="copy_all").pack(anchor=tk.W, pady=3)
ttk.Radiobutton(conv_frame, text="⚡ Copy Video + MP3 Audio (recomendado)", variable=self.conversion_mode, value="copy_video").pack(anchor=tk.W, pady=3)
ttk.Radiobutton(conv_frame, text="🔄 Convertir todo (más lento)", variable=self.conversion_mode, value="convert").pack(anchor=tk.W, pady=3)
audio_frame = ttk.Frame(conv_frame)
audio_frame.pack(fill=tk.X, pady=(10, 0))
ttk.Label(audio_frame, text="Bitrate MP3:").pack(side=tk.LEFT, padx=(0, 10))
ttk.Combobox(audio_frame, textvariable=self.audio_bitrate, values=["128k", "192k", "256k", "320k"], state="readonly", width=10).pack(side=tk.LEFT)
adv_frame = ttk.LabelFrame(tab_general, text="Avanzado", padding="15", style="Card.TFrame")
adv_frame.pack(fill=tk.X)
batch_frame = ttk.Frame(adv_frame)
batch_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(batch_frame, text="Lote Video:").grid(row=0, column=0, padx=(0, 10))
ttk.Spinbox(batch_frame, from_=10, to=200, textvariable=self.video_batch_size, width=10).grid(row=0, column=1, padx=(0, 20))
ttk.Label(batch_frame, text="Lote Audio:").grid(row=0, column=2, padx=(0, 10))
ttk.Spinbox(batch_frame, from_=10, to=200, textvariable=self.audio_batch_size, width=10).grid(row=0, column=3)
ttk.Checkbutton(adv_frame, text="Borrar archivos locales después de subir", variable=self.delete_local).pack(anchor=tk.W)
# Tab Normal
tab_normal = ttk.Frame(self.notebook, padding="10")
self.notebook.add(tab_normal, text="Modo Normal")
input_frame = ttk.LabelFrame(tab_normal, text="Fuente", padding="15", style="Card.TFrame")
input_frame.pack(fill=tk.X, pady=(0, 15))
type_frame = ttk.Frame(input_frame)
type_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Radiobutton(type_frame, text="📁 Archivo", variable=self.input_type, value="file", command=self.toggle_input).pack(side=tk.LEFT, padx=(0, 20))
ttk.Radiobutton(type_frame, text="🌐 URL", variable=self.input_type, value="url", command=self.toggle_input).pack(side=tk.LEFT)
self.file_frame = ttk.Frame(input_frame)
ttk.Button(self.file_frame, text="📁 Seleccionar", command=self.browse_file).pack(anchor=tk.W, pady=(0, 10))
ttk.Label(self.file_frame, textvariable=self.video_file, foreground="#888").pack(fill=tk.X)
self.url_frame = ttk.Frame(input_frame)
ttk.Label(self.url_frame, text="URL del video:").pack(anchor=tk.W, pady=(0, 5))
ttk.Entry(self.url_frame, textvariable=self.video_url, width=70).pack(fill=tk.X)
repo_frame = ttk.LabelFrame(tab_normal, text="Repositorio GitHub (existente)", padding="15", style="Card.TFrame")
repo_frame.pack(fill=tk.X)
ttk.Entry(repo_frame, textvariable=self.repo_url, width=70).pack(fill=tk.X)
# Tab Bulk
tab_bulk = ttk.Frame(self.notebook, padding="10")
self.notebook.add(tab_bulk, text="Modo Bulk")
bulk_frame = ttk.LabelFrame(tab_bulk, text="Lista de Videos", padding="15", style="Card.TFrame")
bulk_frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(bulk_frame, text="Cada video creará su propio repositorio automáticamente", foreground="#ffff00").pack(anchor=tk.W, pady=(0, 10))
self.bulk_list = tk.Listbox(bulk_frame, height=15, bg="#2d2d2d", fg="#e0e0e0")
self.bulk_list.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
btn_bulk = ttk.Frame(bulk_frame)
btn_bulk.pack(fill=tk.X)
ttk.Button(btn_bulk, text="+ Archivos", command=self.add_bulk_files).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(btn_bulk, text="+ URLs", command=self.add_bulk_urls).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(btn_bulk, text="Remover", command=self.remove_bulk).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(btn_bulk, text="Limpiar", command=self.clear_bulk).pack(side=tk.LEFT)
# Botones principales
btn_frame = ttk.Frame(main_frame)
btn_frame.pack(pady=15)
self.start_btn = ttk.Button(btn_frame, text="🚀 Iniciar", style="Accent.TButton", command=self.start_processing)
self.start_btn.pack(side=tk.LEFT, padx=(0, 10), ipady=8, ipadx=25)
self.btn_folder = ttk.Button(btn_frame, text="📂 Abrir Carpeta", command=self.open_folder, state=tk.DISABLED)
self.btn_folder.pack(side=tk.LEFT, padx=(0, 10), ipady=8, ipadx=20)
self.btn_link = ttk.Button(btn_frame, text="📋 Copiar Link", command=self.copy_link, state=tk.DISABLED)
self.btn_link.pack(side=tk.LEFT, ipady=8, ipadx=20)
# Log
log_frame = ttk.LabelFrame(main_frame, text="Registro", padding="15", style="Card.TFrame")
log_frame.pack(fill=tk.BOTH, expand=True)
self.log_text = tk.Text(log_frame, height=15, bg="#1e1e1e", fg="#00ff9d", font=("Consolas", 9), wrap=tk.WORD)
self.log_text.pack(fill=tk.BOTH, expand=True)
self.toggle_input()
self.toggle_mode()
def toggle_mode(self):
if self.mode.get() == "normal":
self.notebook.select(1)
else:
self.notebook.select(2)
def toggle_input(self):
self.file_frame.pack_forget()
self.url_frame.pack_forget()
if self.input_type.get() == "file":
self.file_frame.pack(fill=tk.X)
else:
self.url_frame.pack(fill=tk.X)
def browse_file(self):
filename = filedialog.askopenfilename(
title="Seleccionar video",
filetypes=[("Videos", "*.mp4 *.mkv *.avi *.mov *.ts"), ("Todos", "*.*")]
)
if filename:
self.video_file.set(filename)
self.log(f"✓ Archivo: {os.path.basename(filename)}")
def add_bulk_files(self):
files = filedialog.askopenfilenames(
title="Seleccionar videos",
filetypes=[("Videos", "*.mp4 *.mkv *.avi *.mov *.ts"), ("Todos", "*.*")]
)
for f in files:
if f not in [s['value'] for s in self.bulk_sources]:
self.bulk_sources.append({"type": "file", "value": f})
self.bulk_list.insert(tk.END, f"Archivo: {os.path.basename(f)}")
def add_bulk_urls(self):
dialog = tk.Toplevel(self.root)
dialog.title("Añadir URLs")
dialog.geometry("600x400")
ttk.Label(dialog, text="URLs (una por línea):").pack(pady=10)
text = tk.Text(dialog, height=20)
text.pack(fill=tk.BOTH, expand=True, padx=15, pady=10)
def add():
lines = [l.strip() for l in text.get("1.0", tk.END).splitlines() if l.strip()]
for url in lines:
if url not in [s['value'] for s in self.bulk_sources]:
self.bulk_sources.append({"type": "url", "value": url})
self.bulk_list.insert(tk.END, f"URL: {url[:60]}...")
dialog.destroy()
ttk.Button(dialog, text="Añadir", command=add).pack(pady=10)
def remove_bulk(self):
selected = self.bulk_list.curselection()
for i in reversed(selected):
del self.bulk_sources[i]
self.bulk_list.delete(i)
def clear_bulk(self):
self.bulk_sources.clear()
self.bulk_list.delete(0, tk.END)
def open_folder(self):
if self.last_output_dir and os.path.exists(self.last_output_dir):
if platform.system() == "Windows":
os.startfile(self.last_output_dir)
else:
subprocess.call(["open" if platform.system() == "Darwin" else "xdg-open", self.last_output_dir])
def copy_link(self):
if self.last_direct_link:
self.root.clipboard_clear()
self.root.clipboard_append(self.last_direct_link)
messagebox.showinfo("Copiado", f"Link copiado:\n\n{self.last_direct_link}")
def log(self, msg):
self.log_text.insert(tk.END, f"{msg}\n")
self.log_text.see(tk.END)
self.root.update_idletasks()
def show_progress(self):
if self.progress_popup:
return
self.step_var = tk.StringVar(value="Iniciando...")
self.percent_var = tk.StringVar(value="0%")
self.progress_var = tk.DoubleVar(value=0)
popup = tk.Toplevel(self.root)
popup.title("Progreso")
popup.geometry("500x200")
popup.configure(bg="#1e1e1e")
popup.transient(self.root)
popup.grab_set()
frame = ttk.Frame(popup, padding="25")
frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(frame, textvariable=self.step_var, font=("Segoe UI", 11)).pack(pady=(0, 15))
ttk.Progressbar(frame, variable=self.progress_var, length=420, style="Custom.Horizontal.TProgressbar").pack(pady=(0, 15))
ttk.Label(frame, textvariable=self.percent_var, font=("Segoe UI", 16, "bold"), foreground="#00ff9d").pack()
self.progress_popup = popup
def close_progress(self):
if self.progress_popup:
self.progress_popup.destroy()
self.progress_popup = None
def update_progress(self, value, step=""):
if not self.progress_popup:
return
overall = self.base_progress + (value / 100.0 * self.progress_span)
overall = min(100.0, max(0.0, overall))
self.progress_var.set(overall)
self.percent_var.set(f"{int(overall)}%")
if step:
self.step_var.set(step)
self.root.update_idletasks()
def is_url(self, source):
return str(source).startswith(('http://', 'https://'))
def get_url_headers(self):
return ['-user_agent', 'Mozilla/5.0', '-headers', 'Referer: https://rumble.com/\r\n']
def run_cmd(self, cmd, source=""):
if self.is_url(source):
try:
i = cmd.index('-i')
cmd = cmd[:i] + self.get_url_headers() + cmd[i:]
except ValueError:
pass
return subprocess.run(cmd, capture_output=True, text=True, creationflags=self.subprocess_flags)
def get_duration(self, source):
try:
cmd = ['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'csv=p=0', '-i', source]
result = self.run_cmd(cmd, source)
if result.returncode == 0 and result.stdout.strip():
return float(result.stdout.strip())
except:
pass
return None
def get_metadata(self, source):
try:
cmd = ['ffprobe', '-v', 'error', '-show_format', '-print_format', 'json', '-i', source]
result = self.run_cmd(cmd, source)
if result.returncode != 0:
return None, None
data = json.loads(result.stdout)
tags = data.get('format', {}).get('tags', {})
title = tags.get('title', '').strip() or None
show = tags.get('show', '').strip() or None
return title, show
except:
return None, None
def get_filename_from_url(self, url):
parsed = urlparse(url)
path = unquote(parsed.path)
basename = os.path.basename(path)
if basename and '.' in basename:
return Path(basename).stem
return None
def build_name_for_bulk(self, source, is_file):
if is_file:
base_name = Path(source).stem
else:
base_name = self.get_filename_from_url(source) or "video"
base_name = re.sub(r'\.(mp4|mkv|avi|mov|ts|webm)$', '', base_name, flags=re.IGNORECASE)
title, show = self.get_metadata(source)
parts = [base_name]
if title:
title_clean = re.sub(r'[^\w\s\-\(\)]', '_', title)
title_clean = re.sub(r'\s+', '_', title_clean).strip('_')
parts.append(title_clean)
if show:
show_clean = re.sub(r'[^\w\s\-\(\)]', '_', show)
show_clean = re.sub(r'\s+', '_', show_clean).strip('_')
parts.append(show_clean)
final_name = '_'.join(parts)
self.log(f"📝 Nombre generado: {final_name}")
if title:
self.log(f" • Title: {title}")
if show:
self.log(f" • Show: {show}")
return final_name
def clean_for_folder(self, name):
name = re.sub(r'[<>:"/\\|?*]', '_', name)
return name.strip()[:200]
def clean_for_repo(self, name):
name = re.sub(r'[^a-zA-Z0-9_-]', '-', name)
name = re.sub(r'-+', '-', name)
return name.strip('-')[:100]
def get_audio_streams(self, source):
try:
cmd = ['ffprobe', '-v', 'error', '-select_streams', 'a', '-show_entries', 'stream=index:stream_tags=language', '-of', 'json', '-i', source]
result = self.run_cmd(cmd, source)
data = json.loads(result.stdout)
streams = []
for s in data.get('streams', []):
lang = s.get('tags', {}).get('language', 'und')
streams.append({'index': s['index'], 'lang': lang})
return streams
except:
return []
def convert_audio(self, source, stream_index, output_dir, audio_file, segment_file):
mode = self.conversion_mode.get()
if mode == "copy_all":
codec = ['-c:a', 'copy']
else:
codec = ['-c:a', 'libmp3lame', '-b:a', self.audio_bitrate.get()]
cmd = ['ffmpeg', '-i', source, '-map', f'0:{stream_index}', *codec, '-vn',
'-hls_time', '10', '-hls_list_size', '0', '-hls_segment_filename', str(segment_file),
'-start_number', '1', '-hls_playlist_type', 'vod',
str(audio_file), '-y', '-loglevel', 'error']
result = self.run_cmd(cmd, source)
if result.returncode != 0:
raise Exception(f"Error audio: {result.stderr}")
def convert_video(self, source, output_dir):
video_file = output_dir / "index-v1-a1.m3u8"
segment_file = output_dir / "index-v1-a1_%03d.ts"
if self.conversion_mode.get() in ["copy_all", "copy_video"]:
codec = ['-c:v', 'copy']
else:
codec = ['-c:v', 'libx264', '-preset', 'fast', '-crf', '23']
cmd = ['ffmpeg', '-i', source, *codec, '-map', '0:v?', '-an',
'-hls_time', '10', '-hls_list_size', '0', '-hls_segment_filename', str(segment_file),
'-start_number', '1', '-hls_playlist_type', 'vod',
str(video_file), '-y', '-loglevel', 'error']
result = self.run_cmd(cmd, source)
if result.returncode != 0:
raise Exception(f"Error video: {result.stderr}")
def post_process_playlist(self, path: Path):
if not path.exists():
return
with open(path, 'r', encoding='utf-8') as f:
lines = f.readlines()
if not lines or lines[0].strip() != '#EXTM3U':
return
new_lines = ['#EXTM3U\n']
new_lines.append('#EXT-X-ALLOW-CACHE:YES\n')
new_lines.append('#EXT-X-VERSION:3\n')
for line in lines[1:]:
new_lines.append(line)
with open(path, 'w', encoding='utf-8') as f:
f.writelines(new_lines)
def create_master(self, output_dir, renditions):
master = output_dir / "master.m3u8"
with open(master, 'w', encoding='utf-8') as f:
f.write("#EXTM3U\n")
for i, rend in enumerate(renditions, 1):
f.write(f'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio0",NAME="{rend["name"]}",LANGUAGE="{rend["lang"]}",'
f'AUTOSELECT={rend["autoselect"]},DEFAULT={rend["default"]},CHANNELS="2",URI="index-a{i}.m3u8"\n')
bandwidth = 1381285
frame_rate = "24.000"
codecs = "avc1.64001f,mp3" # MP3
f.write(f'#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH={bandwidth},RESOLUTION=1920x1080,FRAME-RATE={frame_rate},'
f'CODECS="{codecs}",VIDEO-RANGE=SDR,AUDIO="audio0"\n')
f.write("index-v1-a1.m3u8\n")
f.write(f'#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH={bandwidth},RESOLUTION=1280x720,FRAME-RATE={frame_rate},'
f'CODECS="{codecs}",VIDEO-RANGE=SDR,AUDIO="audio0"\n')
f.write("index-v1-a1.m3u8\n")
def create_repo(self, name, token):
url = "https://api.github.com/user/repos"
headers = {"Authorization": f"token {token}"}
data = {"name": name, "private": False}
response = requests.post(url, headers=headers, json=data, timeout=30)
if response.status_code == 201:
repo_data = response.json()
return repo_data['clone_url'], repo_data['html_url']
elif response.status_code == 422:
r = requests.get("https://api.github.com/user", headers=headers)
username = r.json()['login']
return f"https://github.com/{username}/{name}.git", f"https://github.com/{username}/{name}"
else:
raise Exception(f"Error creando repo: {response.status_code}")
def git_upload(self, output_dir, repo_url, token, repo_name):
git_dir = str(output_dir)
if '@' not in repo_url:
remote = repo_url.replace('https://', f'https://{token}@')
else:
remote = repo_url
subprocess.run(['git', 'init'], cwd=git_dir, check=True, creationflags=self.subprocess_flags)
subprocess.run(['git', 'branch', '-M', 'main'], cwd=git_dir, creationflags=self.subprocess_flags)
subprocess.run(['git', 'remote', 'remove', 'origin'], cwd=git_dir, stderr=subprocess.DEVNULL, creationflags=self.subprocess_flags)
subprocess.run(['git', 'remote', 'add', 'origin', remote], cwd=git_dir, check=True, creationflags=self.subprocess_flags)
files = [f for f in os.listdir(git_dir) if os.path.isfile(os.path.join(git_dir, f))]
m3u8_files = [f for f in files if f.endswith('.m3u8')]
video_ts = [f for f in files if f.startswith('index-v1-a1') and f.endswith('.ts')]
audio_ts = [f for f in files if f.startswith('index-a') and f.endswith('.ts') and not f.startswith('index-v')]
if m3u8_files:
subprocess.run(['git', 'add'] + m3u8_files, cwd=git_dir, check=True, creationflags=self.subprocess_flags)
subprocess.run(['git', 'commit', '-m', 'Add playlists'], cwd=git_dir, check=True, creationflags=self.subprocess_flags)
subprocess.run(['git', 'push', '-f', 'origin', 'main'], cwd=git_dir, check=True, creationflags=self.subprocess_flags)
self.update_progress(20, "Playlists subidas")
if video_ts:
batch_size = self.video_batch_size.get()
for i in range(0, len(video_ts), batch_size):
batch = video_ts[i:i+batch_size]
subprocess.run(['git', 'add'] + batch, cwd=git_dir, check=True, creationflags=self.subprocess_flags)
subprocess.run(['git', 'commit', '-m', f'Video batch {i//batch_size + 1}'], cwd=git_dir, check=True, creationflags=self.subprocess_flags)
subprocess.run(['git', 'push', '-f', 'origin', 'main'], cwd=git_dir, check=True, creationflags=self.subprocess_flags)
progress = 20 + (50 * (i + len(batch)) / len(video_ts))
self.update_progress(progress, f"Video batch {i//batch_size + 1}")
if audio_ts:
batch_size = self.audio_batch_size.get()
for i in range(0, len(audio_ts), batch_size):
batch = audio_ts[i:i+batch_size]
subprocess.run(['git', 'add'] + batch, cwd=git_dir, check=True, creationflags=self.subprocess_flags)
subprocess.run(['git', 'commit', '-m', f'Audio batch {i//batch_size + 1}'], cwd=git_dir, check=True, creationflags=self.subprocess_flags)
subprocess.run(['git', 'push', '-f', 'origin', 'main'], cwd=git_dir, check=True, creationflags=self.subprocess_flags)
progress = 70 + (30 * (i + len(batch)) / len(audio_ts))
self.update_progress(progress, f"Audio batch {i//batch_size + 1}")
def process_video(self, source, is_file, is_bulk):
self.log(f"\n{'='*60}")
self.log(f"{'📁 ARCHIVO' if is_file else '🌐 URL'}: {os.path.basename(source) if is_file else source[:80]}")
if is_bulk:
output_name = self.build_name_for_bulk(source, is_file)
else:
if is_file:
output_name = Path(source).stem
else:
output_name = self.get_filename_from_url(source) or "video"
folder_name = self.clean_for_folder(output_name)
repo_name = self.clean_for_repo(output_name)
if is_file:
base_dir = Path(source).parent
else:
base_dir = Path.cwd()
self.output_dir = base_dir / f"{folder_name}_hls"
self.output_dir.mkdir(exist_ok=True)
self.log(f"📁 Carpeta: {self.output_dir.name}")
self.update_progress(5, "Analizando...")
audio_streams = self.get_audio_streams(source)
self.log(f"🎵 Audios detectados: {len(audio_streams)}")
audio_streams.sort(key=lambda x: 0 if x['lang'] in ['eng', 'en'] else 1)
renditions = []
for idx, stream in enumerate(audio_streams):
lang = stream['lang']
name = self.lang_names.get(lang, lang.upper())
if lang == 'und':
name = 'Principal'
renditions.append({
'name': name,
'lang': lang,
'default': "YES" if idx == 0 else "NO",
'autoselect': "YES" if idx == 0 else "NO",
'stream_index': stream['index']
})
if len(renditions) == 1:
self.log("Solo un audio → añadiendo uno falso duplicado")
renditions.append({
'name': 'Original',
'lang': 'und',
'default': "NO",
'autoselect': "NO",
'stream_index': audio_streams[0]['index']
})
self.update_progress(10, "Convirtiendo audios...")
for i, rend in enumerate(renditions, 1):
audio_file = self.output_dir / f"index-a{i}.m3u8"
segment_file = self.output_dir / f"index-a{i}_%03d.ts"
self.convert_audio(source, rend['stream_index'], self.output_dir, audio_file, segment_file)
self.update_progress(50, "Convirtiendo video...")
self.convert_video(source, self.output_dir)
self.update_progress(75, "Ajustando playlists...")
for playlist in self.output_dir.glob("index-*.m3u8"):
self.post_process_playlist(playlist)
self.update_progress(80, "Creando master.m3u8...")
self.create_master(self.output_dir, renditions)
self.update_progress(85, "Subiendo a GitHub...")
token = self.github_token.get().strip()
if is_bulk:
clone_url, html_url = self.create_repo(repo_name, token)
self.log(f"✓ Repo creado: {html_url}")
else:
repo_url = self.repo_url.get().strip()
if not repo_url.endswith('.git'):
repo_url += '.git'
clone_url = repo_url
html_url = repo_url.replace('.git', '')
self.git_upload(self.output_dir, clone_url, token, repo_name)
raw_url = html_url.replace('github.com', 'raw.githubusercontent.com') + '/main/master.m3u8'
self.last_direct_link = raw_url
self.log(f"\n{'='*60}")
self.log(f"✅ COMPLETADO")
self.log(f"📦 Repo: {html_url}")
self.log(f"🎬 Link: {raw_url}")
self.log(f"{'='*60}\n")
if self.delete_local.get():
try:
shutil.rmtree(self.output_dir, onerror=lambda func, path, exc: (os.chmod(path, stat.S_IWRITE), func(path)))
self.log("🗑️ Archivos locales borrados")
except:
pass
self.last_output_dir = str(self.output_dir)
self.update_progress(100, "Completado")
def start_processing(self):
if self.processing:
return
token = self.github_token.get().strip()
if not token:
messagebox.showerror("Error", "Token de GitHub requerido")
return
self.processing = True
self.start_btn.config(state=tk.DISABLED)
self.show_progress()
def process():
try:
if self.mode.get() == "normal":
if self.input_type.get() == "file":
source = self.video_file.get()
if not source or not os.path.exists(source):
raise Exception("Selecciona un archivo válido")
is_file = True
else:
source = self.video_url.get().strip()
if not source:
raise Exception("Ingresa una URL válida")
is_file = False
if not self.repo_url.get().strip():
raise Exception("Ingresa URL del repositorio")
self.process_video(source, is_file, False)
else:
if not self.bulk_sources:
raise Exception("Agrega videos en modo bulk")
if not HAS_REQUESTS:
raise Exception("Instala 'requests' para modo bulk")
total = len(self.bulk_sources)
for idx, item in enumerate(self.bulk_sources, 1):
self.log(f"\n{'='*60}")
self.log(f"VIDEO {idx}/{total}")
self.log(f"{'='*60}")
source = item['value']
is_file = item['type'] == 'file'
self.base_progress = ((idx - 1) / total) * 100
self.progress_span = 100 / total
try:
self.process_video(source, is_file, True)
except Exception as e:
self.log(f"❌ Error: {str(e)}")
self.log(f"\n🎉 BULK COMPLETADO: {total} videos procesados")
self.root.after(0, lambda: self.btn_folder.config(state=tk.NORMAL))
self.root.after(0, lambda: self.btn_link.config(state=tk.NORMAL))
except Exception as e:
self.log(f"\n❌ ERROR: {str(e)}")
self.root.after(0, lambda msg=str(e): messagebox.showerror("Error", msg))
finally:
self.processing = False
self.root.after(0, lambda: self.start_btn.config(state=tk.NORMAL))
self.close_progress()
threading.Thread(target=process, daemon=True).start()
if __name__ == "__main__":
root = TkinterDnD.Tk() if DRAG_DROP_AVAILABLE else tk.Tk()
app = HLSConverterApp(root)
root.mainloop()