import gradio as gr import subprocess import os import json import re import shutil import zipfile from pathlib import Path from urllib.parse import urlparse, unquote try: import requests except ImportError: print("Error: Falta instalar 'requests'") # --- Configuración Global --- subprocess.run(["git", "config", "--global", "user.email", "bot@codeberg.org"], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.run(["git", "config", "--global", "user.name", "Hugging Face Bot"], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # --- Funciones Auxiliares --- def clean_for_folder(name): return re.sub(r'[<>:"/\\|?*]', '_', name).strip()[:200] def clean_for_repo(name): name = re.sub(r'[^a-zA-Z0-9_-]', '-', name) return re.sub(r'-+', '-', name).strip('-')[:100] def get_filename_from_url(url): try: parsed = urlparse(url) path = unquote(parsed.path) basename = os.path.basename(path) if basename and '.' in basename: return Path(basename).stem except: pass return "video" def log_generator(message, log_list): log_list.append(message) return "\n".join(log_list), log_list def detect_audio_streams(source_val): cmd_probe = [ 'ffprobe', '-v', 'error', '-select_streams', 'a', '-show_entries', 'stream=index,codec_name:stream_tags=language,title', '-of', 'json', '-i', source_val ] try: res = subprocess.run(cmd_probe, capture_output=True, text=True, timeout=60) data = json.loads(res.stdout) streams = data.get('streams', []) audio_tracks = [] for stream in streams: index = stream.get('index', 0) tags = stream.get('tags', {}) language = tags.get('language', 'und') title = tags.get('title', f'Audio {index}') codec = stream.get('codec_name', 'unknown') audio_tracks.append({'index': index, 'language': language.lower(), 'title': title, 'codec': codec}) return audio_tracks except Exception as e: return [] def prioritize_audio_tracks(tracks): priority_langs = ['spa', 'es', 'spanish', 'español', 'latino', 'lat', 'es-mx', 'es-419'] def get_priority(track): lang = track['language'].lower() title = track['title'].lower() for term in priority_langs: if term in lang or term in title: return 0 if lang == 'und': return 1 return 2 return sorted(tracks, key=get_priority) def create_master_m3u8(output_dir, video_streams, audio_playlists): master_content = "#EXTM3U\n#EXT-X-VERSION:7\n\n" for i, audio_info in enumerate(audio_playlists): default = "YES" if audio_info['is_default'] else "NO" autoselect = "YES" if audio_info['is_default'] else "NO" master_content += f'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="{audio_info["title"]}",LANGUAGE="{audio_info["language"]}",DEFAULT={default},AUTOSELECT={autoselect},URI="{audio_info["file"]}"\n' master_content += "\n" for vid in video_streams: master_content += f'#EXT-X-STREAM-INF:BANDWIDTH={vid["bandwidth"]},RESOLUTION={vid["resolution"]},CODECS="{vid["codecs"]}",AUDIO="audio"\n{vid["file"]}\n' with open(output_dir / "master.m3u8", "w") as f: f.write(master_content) # --- Funciones de Subida --- def upload_to_codeberg(output_dir, repo_name, codeberg_token, username, batch_size, stream_format, logs): try: msg, logs = log_generator("📦 Creando repositorio en Codeberg...", logs) yield msg, logs, None url_api = f"https://codeberg.org/api/v1/user/repos" headers = {"Authorization": f"token {codeberg_token}", "Content-Type": "application/json"} data = {"name": repo_name, "private": True, "auto_init": False} try: r = requests.post(url_api, headers=headers, json=data, timeout=30) except: pass repo_url = f"https://codeberg.org/{username}/{repo_name}" git_dir = str(output_dir) git_auth_url = repo_url.replace('https://', f'https://{username}:{codeberg_token}@') subprocess.run(['git', 'init'], cwd=git_dir, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.run(['git', 'remote', 'add', 'origin', git_auth_url], cwd=git_dir, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.run(['git', 'config', 'http.postBuffer', '524288000'], cwd=git_dir, check=True) files = os.listdir(git_dir) if stream_format == "DASH (MPD)": seg_files = [f for f in files if f.endswith('.m4s')] manifest_files = [f for f in files if f.endswith('.mpd')] manifest_name = "manifest.mpd" else: seg_files = [f for f in files if f.endswith('.ts')] manifest_files = [f for f in files if f.endswith('.m3u8')] manifest_name = "master.m3u8" if manifest_files: subprocess.run(['git', 'add'] + manifest_files, cwd=git_dir, check=True) subprocess.run(['git', 'commit', '-m', 'Add manifests'], cwd=git_dir, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) try: subprocess.run(['git', 'push', '-f', 'origin', 'HEAD:main'], cwd=git_dir, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=600) except: pass msg, logs = log_generator(f"⬆️ Subiendo {len(seg_files)} segmentos...", logs) yield msg, logs, None for i in range(0, len(seg_files), batch_size): batch = seg_files[i:i+batch_size] subprocess.run(['git', 'add'] + batch, cwd=git_dir, check=True) subprocess.run(['git', 'commit', '-m', f'Batch {i}'], cwd=git_dir, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) try: subprocess.run(['git', 'push', '-f', 'origin', 'HEAD:main'], cwd=git_dir, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=600) except: pass final_url = f"{repo_url}/raw/branch/main/{manifest_name}" msg, logs = log_generator(f"✅ Codeberg: {final_url}", logs) yield msg, logs, final_url except Exception as e: msg, logs = log_generator(f"❌ Error Codeberg: {str(e)}", logs) yield msg, logs, None def upload_to_cloudflare_pages(output_dir, project_name, cf_token, cf_account_id, stream_format, logs): try: msg, logs = log_generator("☁️ Iniciando subida a Cloudflare Pages...", logs) yield msg, logs, None headers = { "Authorization": f"Bearer {cf_token}", "Content-Type": "application/json" } # PASO 1: Crear el proyecto en Cloudflare Pages si no existe msg, logs = log_generator("📦 Verificando proyecto en Cloudflare...", logs) yield msg, logs, None project_url = f"https://api.cloudflare.com/client/v4/accounts/{cf_account_id}/pages/projects" project_exists = False try: r = requests.get(project_url, headers=headers, timeout=30) if r.status_code == 200: projects = r.json().get('result', []) project_exists = any(p['name'] == project_name for p in projects) if not project_exists: msg, logs = log_generator(f"📦 Creando proyecto: {project_name}", logs) yield msg, logs, None project_data = { "name": project_name, "production_branch": "main" } r = requests.post(project_url, headers=headers, json=project_data, timeout=30) if r.status_code in [200, 201]: msg, logs = log_generator(f"✅ Proyecto creado exitosamente", logs) yield msg, logs, None project_exists = True else: error_msg = r.json().get('errors', [{}])[0].get('message', 'Error desconocido') raise Exception(f"Error creando proyecto: {error_msg}") else: msg, logs = log_generator(f"✅ Proyecto ya existe", logs) yield msg, logs, None else: raise Exception(f"Error verificando proyectos: {r.status_code}") except Exception as e: msg, logs = log_generator(f"❌ Error en paso 1: {str(e)}", logs) yield msg, logs, None raise if not project_exists: raise Exception("No se pudo crear o verificar el proyecto") # PASO 2: Iniciar deployment y subir archivos msg, logs = log_generator("📦 Preparando deployment...", logs) yield msg, logs, None # Listar archivos a subir files_to_upload = [] for file in output_dir.iterdir(): if file.is_file(): files_to_upload.append(file) msg, logs = log_generator(f"📁 {len(files_to_upload)} archivos para subir", logs) yield msg, logs, None # Crear manifest con hashes de archivos import hashlib manifest = {} for file in files_to_upload: with open(file, 'rb') as f: file_hash = hashlib.sha256(f.read()).hexdigest() manifest[f"/{file.name}"] = file_hash # Iniciar deployment con Direct Upload deployment_url = f"https://api.cloudflare.com/client/v4/accounts/{cf_account_id}/pages/projects/{project_name}/deployments" deployment_data = { "branch": "main", "manifest": manifest } msg, logs = log_generator("🚀 Iniciando deployment...", logs) yield msg, logs, None r = requests.post(deployment_url, headers=headers, json=deployment_data, timeout=60) if r.status_code not in [200, 201]: error_detail = r.json() if r.content else "Sin detalles" raise Exception(f"Error iniciando deployment ({r.status_code}): {error_detail}") deployment = r.json().get('result', {}) deployment_id = deployment.get('id') if not deployment_id: raise Exception("No se recibió deployment_id") msg, logs = log_generator(f"✅ Deployment iniciado: {deployment_id[:8]}...", logs) yield msg, logs, None # PASO 3: Subir cada archivo msg, logs = log_generator(f"⬆️ Subiendo archivos...", logs) yield msg, logs, None uploaded_count = 0 for idx, file in enumerate(files_to_upload): try: # URL para subir archivo individual file_upload_url = f"https://api.cloudflare.com/client/v4/accounts/{cf_account_id}/pages/projects/{project_name}/deployments/{deployment_id}/files/{file.name}" with open(file, 'rb') as f: file_content = f.read() upload_headers = { "Authorization": f"Bearer {cf_token}", "Content-Type": "application/octet-stream" } r = requests.put(file_upload_url, headers=upload_headers, data=file_content, timeout=120) if r.status_code in [200, 201]: uploaded_count += 1 if (idx + 1) % 10 == 0: msg, logs = log_generator(f" ⬆️ {uploaded_count}/{len(files_to_upload)} archivos subidos", logs) yield msg, logs, None else: msg, logs = log_generator(f" ⚠️ Error subiendo {file.name}: {r.status_code}", logs) yield msg, logs, None except Exception as e: msg, logs = log_generator(f" ⚠️ Error con {file.name}: {str(e)}", logs) yield msg, logs, None continue msg, logs = log_generator(f"✅ {uploaded_count}/{len(files_to_upload)} archivos subidos", logs) yield msg, logs, None # PASO 4: Finalizar deployment msg, logs = log_generator("🏁 Finalizando deployment...", logs) yield msg, logs, None finalize_url = f"https://api.cloudflare.com/client/v4/accounts/{cf_account_id}/pages/projects/{project_name}/deployments/{deployment_id}/finalize" r = requests.post(finalize_url, headers=headers, timeout=60) if r.status_code in [200, 201]: result = r.json().get('result', {}) cf_url = result.get('url', f"https://{project_name}.pages.dev") manifest_name = "manifest.mpd" if stream_format == "DASH (MPD)" else "master.m3u8" final_url = f"{cf_url}/{manifest_name}" msg, logs = log_generator(f"✅ Cloudflare Pages: {final_url}", logs) yield msg, logs, final_url else: # Aunque falle la finalización, el proyecto está creado manifest_name = "manifest.mpd" if stream_format == "DASH (MPD)" else "master.m3u8" final_url = f"https://{project_name}.pages.dev/{manifest_name}" msg, logs = log_generator(f"⚠️ Deployment puede estar procesando", logs) yield msg, logs, None msg, logs = log_generator(f"📋 URL esperada: {final_url}", logs) yield msg, logs, final_url except Exception as e: msg, logs = log_generator(f"❌ Error Cloudflare Pages: {str(e)}", logs) yield msg, logs, None # Proporcionar URL esperada como fallback try: manifest_name = "manifest.mpd" if stream_format == "DASH (MPD)" else "master.m3u8" final_url = f"https://{project_name}.pages.dev/{manifest_name}" msg, logs = log_generator(f"📋 Si el proyecto se creó, la URL será: {final_url}", logs) yield msg, logs, None except: pass # --- Procesamiento Principal --- def process_video(file_input, url_input, codeberg_token, cf_token, cf_account_id, conversion_mode, stream_format, upload_codeberg_flag, upload_cloudflare_flag, batch_size, delete_local, progress=gr.Progress()): logs = ["🚀 Iniciando conversión..."] try: # Validar entrada source = None is_url = False if file_input: source = file_input.name elif url_input: source = url_input is_url = True else: return "\n".join(logs + ["❌ Selecciona archivo o URL"]), "Error" # Validar destinos if not upload_codeberg_flag and not upload_cloudflare_flag: return "\n".join(logs + ["❌ Selecciona al menos un destino"]), "Error" if upload_codeberg_flag and not codeberg_token: return "\n".join(logs + ["❌ Se requiere Token de Codeberg"]), "Error" if upload_cloudflare_flag and (not cf_token or not cf_account_id): return "\n".join(logs + ["❌ Se requiere Token y Account ID de Cloudflare"]), "Error" # Validar usuario Codeberg username = None if upload_codeberg_flag: headers_cb = {"Authorization": f"token {codeberg_token}"} try: user_resp = requests.get("https://codeberg.org/api/v1/user", headers=headers_cb, timeout=10) if user_resp.status_code != 200: raise Exception("Token de Codeberg inválido") username = user_resp.json().get('login') logs.append(f"👤 Usuario: {username}") except Exception as e: return "\n".join(logs + [f"❌ {str(e)}"]), "Error" # Preparar nombres base_name = Path(source).stem if not is_url else get_filename_from_url(source) repo_name = clean_for_repo(base_name) folder_name = clean_for_folder(base_name) + f"_{stream_format.lower().replace(' ', '_')}" output_dir = Path.cwd() / folder_name if output_dir.exists(): shutil.rmtree(output_dir) output_dir.mkdir(exist_ok=True) logs.append(f"📹 Procesando: {base_name}") yield "\n".join(logs), "Procesando" # Detectar audio audio_tracks = detect_audio_streams(source) if not audio_tracks: audio_tracks = [{'index': 0, 'language': 'und', 'title': 'Audio', 'codec': 'unknown'}] audio_tracks = prioritize_audio_tracks(audio_tracks) logs.append(f"🎵 {len(audio_tracks)} streams de audio detectados") yield "\n".join(logs), "Procesando" # Conversión FFmpeg if stream_format == "HLS (M3U8)": # Procesar audio audio_playlists = [] for i, track in enumerate(audio_tracks): audio_file = output_dir / f"audio_{i}.m3u8" seg_audio = output_dir / f"audio_{i}_%03d.ts" if conversion_mode == "Copy Video + Copy Audio": audio_params = ['-c:a', 'copy'] else: audio_params = ['-c:a', 'libmp3lame', '-b:a', '192k', '-ar', '48000'] cmd_audio = ['ffmpeg', '-i', source, '-map', f"0:{track['index']}"] + audio_params + [ '-hls_time', '10', '-hls_list_size', '0', '-hls_segment_filename', str(seg_audio), str(audio_file), '-y', '-loglevel', 'warning' ] subprocess.run(cmd_audio, capture_output=True, timeout=3600) audio_playlists.append({'file': f"audio_{i}.m3u8", 'language': track['language'], 'title': track['title'], 'is_default': i == 0}) # Procesar video video_streams = [] if conversion_mode in ["Copy Video + Copy Audio", "Copy Video + MP3 Audio"]: video_file = output_dir / "video_1080p.m3u8" seg_video = output_dir / "video_1080p_%03d.ts" cmd_video = ['ffmpeg', '-i', source, '-map', '0:v', '-c:v', 'copy', '-an', '-hls_time', '10', '-hls_list_size', '0', '-hls_segment_filename', str(seg_video), str(video_file), '-y', '-loglevel', 'warning'] subprocess.run(cmd_video, capture_output=True, timeout=7200) video_streams.append({'file': 'video_1080p.m3u8', 'resolution': '1920x1080', 'bandwidth': 5000000, 'codecs': 'avc1.640028'}) elif conversion_mode == "Multi-Res (1080p + 720p)": for res in [{'label': '1080p', 'scale': 'scale=-2:1080', 'br': '5000k', 'res': '1920x1080'}, {'label': '720p', 'scale': 'scale=-2:720', 'br': '2800k', 'res': '1280x720'}]: video_file = output_dir / f"video_{res['label']}.m3u8" seg_video = output_dir / f"video_{res['label']}_%03d.ts" cmd_video = ['ffmpeg', '-i', source, '-map', '0:v', '-an', '-c:v', 'libx264', '-preset', 'medium', '-crf', '20', '-vf', res['scale'], '-b:v', res['br'], '-maxrate', res['br'], '-bufsize', str(int(res['br'].replace('k',''))*2) + 'k', '-pix_fmt', 'yuv420p', '-hls_time', '10', '-hls_list_size', '0', '-hls_segment_filename', str(seg_video), str(video_file), '-y', '-loglevel', 'warning'] subprocess.run(cmd_video, capture_output=True, timeout=7200) video_streams.append({'file': f"video_{res['label']}.m3u8", 'resolution': res['res'], 'bandwidth': int(res['br'].replace('k', '000')) + 192000, 'codecs': 'avc1.640028'}) create_master_m3u8(output_dir, video_streams, audio_playlists) elif stream_format == "DASH (MPD)": cmd = ['ffmpeg', '-i', source] is_copy = (conversion_mode == "Copy Video + Copy Audio") if is_copy: cmd.extend(['-map', '0:v:0', '-c:v:0', 'copy']) else: if conversion_mode == "Multi-Res (1080p + 720p)": for idx, r in enumerate([{'scale': 'scale=-2:1080', 'br': '5000k'}, {'scale': 'scale=-2:720', 'br': '2800k'}]): cmd.extend(['-map', '0:v:0', f'-c:v:{idx}', 'libx264', '-preset', 'medium', '-crf', '20', f'-vf:v:{idx}', r['scale'], f'-b:v:{idx}', r['br'], '-pix_fmt', 'yuv420p']) for i, track in enumerate(audio_tracks): cmd.extend(['-map', f"0:{track['index']}", f'-c:a:{i}', 'aac' if not is_copy else 'copy', '-b:a:192k', '-ar', '48000']) mpd_output = output_dir / "manifest.mpd" cmd.extend(['-f', 'dash', '-seg_duration', '10', '-use_template', '1', '-use_timeline', '0', '-init_seg_name', 'init-$RepresentationID$.m4s', '-media_seg_name', 'chunk-$RepresentationID$-$Number%05d$.m4s', str(mpd_output), '-y', '-loglevel', 'warning']) subprocess.run(cmd, capture_output=True, timeout=7200) logs.append("✅ Conversión completada") yield "\n".join(logs), "Subiendo" # Subir a destinos result_links = [] if upload_codeberg_flag: for update in upload_to_codeberg(output_dir, repo_name, codeberg_token, username, int(batch_size), stream_format, logs): yield update[0], "Subiendo" if update[2]: result_links.append(f"📂 Codeberg: {update[2]}") if upload_cloudflare_flag: for update in upload_to_cloudflare_pages(output_dir, repo_name, cf_token, cf_account_id, stream_format, logs): yield update[0], "Subiendo" if update[2]: result_links.append(f"☁️ Cloudflare: {update[2]}") if delete_local: shutil.rmtree(output_dir) logs.append("🗑️ Archivos locales eliminados") final_output = "\n".join(logs + ["", "🔗 Enlaces:"] + result_links) return final_output, "¡Listo!" except Exception as e: import traceback traceback.print_exc() return "\n".join(logs + [f"❌ ERROR: {str(e)}"]), "Fallo" # --- Interfaz Gradio --- with gr.Blocks(title="Video Streaming Converter", theme=gr.themes.Soft()) as demo: gr.Markdown("# 🎬 Video Streaming Converter") gr.Markdown("Convierte videos a HLS/DASH y súbelos a Codeberg o Cloudflare Pages") with gr.Row(): with gr.Column(scale=1): # Tokens codeberg_token = gr.Textbox( label="Codeberg Token", value="92427ac14a228f0762ec303d478b9f093be4f608", type="password", placeholder="Token con permisos 'repo'" ) cf_token = gr.Textbox( label="Cloudflare Token", value="mOvchd-yxYyQ6Zj3xMb_38Rkf-HwROchlsx-Ud9H", type="password", placeholder="Token de Cloudflare Pages" ) cf_account_id = gr.Textbox( label="Cloudflare Account ID", value="bd06ac4017668e45b656db342029929d", placeholder="ID de tu cuenta" ) # Modo conversion_mode = gr.Radio( choices=["Copy Video + Copy Audio", "Copy Video + MP3 Audio", "Multi-Res (1080p + 720p)"], value="Multi-Res (1080p + 720p)", label="Modo" ) stream_format = gr.Radio( choices=["HLS (M3U8)", "DASH (MPD)"], value="HLS (M3U8)", label="Formato" ) # Destino upload_codeberg = gr.Checkbox(label="Subir a Codeberg", value=True) upload_cloudflare = gr.Checkbox(label="Subir a Cloudflare Pages", value=False) # Opciones avanzadas with gr.Accordion("Opciones Avanzadas", open=False): batch_size = gr.Number(value=20, label="Batch Size", precision=0) delete_local = gr.Checkbox(value=True, label="Borrar archivos locales") with gr.Column(scale=2): # Entrada with gr.Tab("Archivo"): file_input = gr.File(label="Subir Video", file_types=["video"]) with gr.Tab("URL"): url_input = gr.Textbox(label="URL del Video", placeholder="https://ejemplo.com/video.mp4") # Botón btn_process = gr.Button("🚀 PROCESAR Y SUBIR", variant="primary", size="lg") # Outputs log_output = gr.Textbox(label="Log", lines=15, interactive=False) status_output = gr.Textbox(label="Estado", interactive=False) gr.Markdown("---") gr.Markdown("💡 **Tip:** Cloudflare Pages es rápido pero público. Codeberg es privado pero más lento.") # Eventos btn_process.click( fn=process_video, inputs=[ file_input, url_input, codeberg_token, cf_token, cf_account_id, conversion_mode, stream_format, upload_codeberg, upload_cloudflare, batch_size, delete_local ], outputs=[log_output, status_output] ) if __name__ == "__main__": demo.launch()