codes / app.py
CineMax's picture
Update app.py
2d666e8 verified
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()