CineMax commited on
Commit
f7fbb72
·
verified ·
1 Parent(s): 31d9a80

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +219 -356
app.py CHANGED
@@ -11,7 +11,7 @@ from urllib.parse import urlparse, unquote
11
  try:
12
  import requests
13
  except ImportError:
14
- print("Error: Falta instalar 'requests'. Ejecuta: pip install requests")
15
 
16
  # --- Configuración Global ---
17
  subprocess.run(["git", "config", "--global", "user.email", "bot@codeberg.org"], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
@@ -91,7 +91,6 @@ def create_master_m3u8(output_dir, video_streams, audio_playlists):
91
  # --- Funciones de Subida ---
92
 
93
  def upload_to_codeberg(output_dir, repo_name, codeberg_token, username, batch_size, stream_format, logs):
94
- """Sube archivos a Codeberg via Git"""
95
  try:
96
  msg, logs = log_generator("📦 Creando repositorio en Codeberg...", logs)
97
  yield msg, logs, None
@@ -102,12 +101,7 @@ def upload_to_codeberg(output_dir, repo_name, codeberg_token, username, batch_si
102
 
103
  try:
104
  r = requests.post(url_api, headers=headers, json=data, timeout=30)
105
- if r.status_code not in [200, 201, 409]:
106
- msg, logs = log_generator(f"⚠️ API Codeberg: {r.text[:100]}", logs)
107
- yield msg, logs, None
108
- except Exception as e:
109
- msg, logs = log_generator(f"⚠️ Error API: {str(e)}", logs)
110
- yield msg, logs, None
111
 
112
  repo_url = f"https://codeberg.org/{username}/{repo_name}"
113
  git_dir = str(output_dir)
@@ -130,16 +124,20 @@ def upload_to_codeberg(output_dir, repo_name, codeberg_token, username, batch_si
130
  if manifest_files:
131
  subprocess.run(['git', 'add'] + manifest_files, cwd=git_dir, check=True)
132
  subprocess.run(['git', 'commit', '-m', 'Add manifests'], cwd=git_dir, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
133
- subprocess.run(['git', 'push', '-f', 'origin', 'HEAD:main'], cwd=git_dir, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=600)
 
 
134
 
135
- msg, logs = log_generator(f"⬆️ Subiendo {len(seg_files)} segmentos a Codeberg...", logs)
136
  yield msg, logs, None
137
 
138
  for i in range(0, len(seg_files), batch_size):
139
  batch = seg_files[i:i+batch_size]
140
  subprocess.run(['git', 'add'] + batch, cwd=git_dir, check=True)
141
  subprocess.run(['git', 'commit', '-m', f'Batch {i}'], cwd=git_dir, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
142
- subprocess.run(['git', 'push', '-f', 'origin', 'HEAD:main'], cwd=git_dir, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=600)
 
 
143
 
144
  final_url = f"{repo_url}/raw/branch/main/{manifest_name}"
145
  msg, logs = log_generator(f"✅ Codeberg: {final_url}", logs)
@@ -150,16 +148,11 @@ def upload_to_codeberg(output_dir, repo_name, codeberg_token, username, batch_si
150
  yield msg, logs, None
151
 
152
  def upload_to_cloudflare_pages(output_dir, project_name, cf_token, cf_account_id, stream_format, logs):
153
- """Sube archivos directamente a Cloudflare Pages"""
154
  try:
155
- msg, logs = log_generator("☁️ Preparando subida a Cloudflare Pages...", logs)
156
  yield msg, logs, None
157
 
158
- headers = {
159
- "Authorization": f"Bearer {cf_token}",
160
- "Content-Type": "application/json"
161
- }
162
-
163
  project_url = f"https://api.cloudflare.com/client/v4/accounts/{cf_account_id}/pages/projects"
164
 
165
  try:
@@ -167,24 +160,10 @@ def upload_to_cloudflare_pages(output_dir, project_name, cf_token, cf_account_id
167
  if r.status_code == 200:
168
  projects = r.json().get('result', [])
169
  exists = any(p['name'] == project_name for p in projects)
170
-
171
  if not exists:
172
- msg, logs = log_generator(f"📦 Creando proyecto: {project_name}", logs)
173
- yield msg, logs, None
174
-
175
- project_data = {
176
- "name": project_name,
177
- "production_branch": "main"
178
- }
179
- r = requests.post(project_url, headers=headers, json=project_data, timeout=30)
180
- if r.status_code not in [200, 201]:
181
- raise Exception(f"Error creando proyecto: {r.text[:200]}")
182
- except Exception as e:
183
- msg, logs = log_generator(f"⚠️ {str(e)}", logs)
184
- yield msg, logs, None
185
-
186
- msg, logs = log_generator("📦 Comprimiendo archivos...", logs)
187
- yield msg, logs, None
188
 
189
  zip_path = output_dir.parent / f"{project_name}.zip"
190
  with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
@@ -192,381 +171,265 @@ def upload_to_cloudflare_pages(output_dir, project_name, cf_token, cf_account_id
192
  if file.is_file():
193
  zipf.write(file, file.name)
194
 
195
- msg, logs = log_generator("⬆️ Subiendo a Cloudflare Pages...", logs)
196
- yield msg, logs, None
197
-
198
  deploy_url = f"{project_url}/{project_name}/deployments"
199
 
200
  with open(zip_path, 'rb') as f:
201
  files_upload = {'file': (f'{project_name}.zip', f, 'application/zip')}
202
- headers_upload = {"Authorization": f"Bearer {cf_token}"}
203
-
204
- r = requests.post(deploy_url, headers=headers_upload, files=files_upload, timeout=300)
205
 
206
  if r.status_code in [200, 201]:
207
  result = r.json().get('result', {})
208
  cf_url = result.get('url', f"https://{project_name}.pages.dev")
209
  manifest_name = "manifest.mpd" if stream_format == "DASH (MPD)" else "master.m3u8"
210
  final_url = f"{cf_url}/{manifest_name}"
211
- msg, logs = log_generator(f"✅ Cloudflare Pages: {final_url}", logs)
212
  yield msg, logs, final_url
213
-
214
  zip_path.unlink()
215
  else:
216
- raise Exception(f"Error en deployment: {r.status_code} - {r.text[:200]}")
217
 
218
  except Exception as e:
219
  msg, logs = log_generator(f"❌ Error Cloudflare: {str(e)}", logs)
220
  yield msg, logs, None
221
 
222
- # --- Lógica de Procesamiento Principal ---
223
 
224
- def process_video(
225
- codeberg_token, cf_token, cf_account_id, input_file, input_urls,
226
- conversion_option, stream_format, batch_size_vid, delete_local,
227
- upload_codeberg_flag, upload_cloudflare_flag,
228
- progress=gr.Progress()
229
- ):
230
- logs = ["🚀 Iniciando proceso..."]
231
-
232
- if not upload_codeberg_flag and not upload_cloudflare_flag:
233
- logs.append("❌ Selecciona al menos un destino (Codeberg o Cloudflare)")
234
- yield "\n".join(logs), logs, ""
235
- return
236
 
237
- if upload_codeberg_flag and not codeberg_token:
238
- logs.append("❌ Se requiere Token de Codeberg")
239
- yield "\n".join(logs), logs, ""
240
- return
241
 
242
- if upload_cloudflare_flag and (not cf_token or not cf_account_id):
243
- logs.append("❌ Se requiere Token y Account ID de Cloudflare")
244
- yield "\n".join(logs), logs, ""
245
- return
246
-
247
- username = None
248
- if upload_codeberg_flag:
249
- headers_cb = {"Authorization": f"token {codeberg_token}"}
250
- try:
251
- user_resp = requests.get("https://codeberg.org/api/v1/user", headers=headers_cb, timeout=10)
252
- if user_resp.status_code != 200:
253
- raise Exception("❌ Token de Codeberg inválido")
254
- username = user_resp.json().get('login')
255
- logs.append(f"👤 Usuario Codeberg: {username}")
256
- yield "\n".join(logs), logs, ""
257
- except Exception as e:
258
- logs.append(str(e))
259
- yield "\n".join(logs), logs, ""
260
- return
261
-
262
- sources = []
263
- if input_file:
264
- files_list = input_file if isinstance(input_file, list) else [input_file]
265
- for f in files_list:
266
- if f is not None: sources.append({"type": "file", "value": f.name})
267
-
268
- if input_urls:
269
- for line in input_urls.split('\n'):
270
- url = line.strip()
271
- if url: sources.append({"type": "url", "value": url})
272
-
273
- if not sources:
274
- logs.append("⚠️ Agrega archivos o URLs para procesar")
275
- yield "\n".join(logs), logs, ""
276
- return
277
-
278
- total_sources = len(sources)
279
- final_links = []
280
-
281
- for idx, source in enumerate(sources):
282
- try:
283
- is_file = source['type'] == 'file'
284
- source_val = source['value']
285
- base_name = Path(source_val).stem if is_file else get_filename_from_url(source_val)
286
-
287
- msg, logs = log_generator(f"\n📹 [{idx+1}/{total_sources}] Procesando: {base_name}", logs)
288
- yield msg, logs, ""
289
-
290
- repo_name = clean_for_repo(base_name)
291
- folder_name = clean_for_folder(base_name) + f"_{stream_format.lower().replace(' ', '_')}"
292
- output_dir = Path.cwd() / folder_name
293
-
294
- if output_dir.exists():
295
- shutil.rmtree(output_dir)
296
- output_dir.mkdir(exist_ok=True)
297
-
298
- msg, logs = log_generator("🔍 Detectando streams de audio...", logs)
299
- yield msg, logs, ""
300
-
301
  try:
302
- audio_tracks = detect_audio_streams(source_val)
303
- except:
304
- audio_tracks = []
305
-
306
- if not audio_tracks:
307
- msg, logs = log_generator("⚠️ No se detectaron streams, usando track 0", logs)
308
- yield msg, logs, ""
309
- audio_tracks = [{'index': 0, 'language': 'und', 'title': 'Audio', 'codec': 'unknown'}]
310
-
311
- audio_tracks = prioritize_audio_tracks(audio_tracks)
312
-
313
- # --- Conversión FFmpeg (CÓDIGO COMPLETO IGUAL QUE ANTES) ---
314
- if stream_format == "HLS (M3U8)":
315
- audio_playlists = []
316
- msg, logs = log_generator("🎵 Procesando audio (HLS)...", logs)
317
- yield msg, logs, ""
318
-
319
- for i, track in enumerate(audio_tracks):
320
- try:
321
- audio_file = output_dir / f"audio_{i}.m3u8"
322
- seg_audio = output_dir / f"audio_{i}_%03d.ts"
323
-
324
- if conversion_option == "Opción 1: Copy Video + Copy Audio":
325
- audio_params = ['-c:a', 'copy']
326
- elif conversion_option == "Opción 2: Copy Video + MP3 Audio":
327
- audio_params = ['-c:a', 'libmp3lame', '-b:a', '192k', '-ar', '48000']
328
- else:
329
- audio_params = ['-c:a', 'aac', '-b:a', '128k']
330
-
331
- cmd_audio = ['ffmpeg', '-i', source_val, '-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']
332
- subprocess.run(cmd_audio, capture_output=True, text=True, timeout=3600)
333
- audio_playlists.append({'file': f"audio_{i}.m3u8", 'language': track['language'], 'title': track['title'], 'is_default': i == 0})
334
- except Exception as e:
335
- msg, logs = log_generator(f" ❌ Error Audio {i+1}: {str(e)}", logs)
336
- yield msg, logs, ""
337
-
338
- if not audio_playlists:
339
- raise Exception("No se pudo codificar audio")
340
-
341
- video_streams = []
342
- msg, logs = log_generator("🎬 Procesando video...", logs)
343
- yield msg, logs, ""
344
-
345
- if conversion_option in ["Opción 1: Copy Video + Copy Audio", "Opción 2: Copy Video + MP3 Audio"]:
346
- video_file_1080 = output_dir / "video_1080p.m3u8"
347
- seg_video_1080 = output_dir / "video_1080p_%03d.ts"
348
- cmd_video = ['ffmpeg', '-i', source_val, '-map', '0:v', '-c:v', 'copy', '-an', '-hls_time', '10', '-hls_list_size', '0', '-hls_segment_filename', str(seg_video_1080), str(video_file_1080), '-y', '-loglevel', 'warning']
349
- subprocess.run(cmd_video, capture_output=True, text=True, timeout=7200)
350
-
351
- video_file_720 = output_dir / "video_720p.m3u8"
352
- shutil.copy(video_file_1080, video_file_720)
353
- for ts_file in output_dir.glob("video_1080p_*.ts"):
354
- shutil.copy(ts_file, output_dir / ts_file.name.replace("1080p", "720p"))
355
-
356
- video_streams.append({'file': 'video_1080p.m3u8', 'resolution': '1920x1080', 'bandwidth': 5000000, 'codecs': 'avc1.640028'})
357
- video_streams.append({'file': 'video_720p.m3u8', 'resolution': '1280x720', 'bandwidth': 3000000, 'codecs': 'avc1.640028'})
358
-
359
- elif conversion_option == "Opción 3: Multi-Res (1080p + 720p) H.264":
360
- res_list = [
361
- {'label': '1080p', 'scale': 'scale=-2:1080', 'br': '5000k', 'res': '1920x1080'},
362
- {'label': '720p', 'scale': 'scale=-2:720', 'br': '2800k', 'res': '1280x720'}
363
- ]
364
- for res_config in res_list:
365
- msg, logs = log_generator(f" 🔄 Renderizando {res_config['label']}...", logs)
366
- yield msg, logs, ""
367
- video_file = output_dir / f"video_{res_config['label']}.m3u8"
368
- seg_video = output_dir / f"video_{res_config['label']}_%03d.ts"
369
- cmd_video = ['ffmpeg', '-i', source_val, '-map', '0:v', '-an', '-c:v', 'libx264', '-preset', 'medium', '-crf', '20', '-vf', res_config['scale'], '-b:v', res_config['br'], '-maxrate', res_config['br'], '-bufsize', str(int(res_config['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']
370
- subprocess.run(cmd_video, capture_output=True, text=True, timeout=7200)
371
- video_streams.append({'file': f"video_{res_config['label']}.m3u8", 'resolution': res_config['res'], 'bandwidth': int(res_config['br'].replace('k', '000')) + 192000, 'codecs': 'avc1.640028'})
372
-
373
- elif conversion_option == "Opción 4: Multi-Res (4K + 1080p + 720p) H.264":
374
- res_list = [
375
- {'label': '4K', 'scale': 'scale=-2:2160', 'br': '15000k', 'res': '3840x2160'},
376
- {'label': '1080p', 'scale': 'scale=-2:1080', 'br': '5000k', 'res': '1920x1080'},
377
- {'label': '720p', 'scale': 'scale=-2:720', 'br': '2800k', 'res': '1280x720'}
378
- ]
379
- for res_config in res_list:
380
- msg, logs = log_generator(f" 🔄 Renderizando {res_config['label']}...", logs)
381
- yield msg, logs, ""
382
- video_file = output_dir / f"video_{res_config['label']}.m3u8"
383
- seg_video = output_dir / f"video_{res_config['label']}_%03d.ts"
384
- cmd_video = ['ffmpeg', '-i', source_val, '-map', '0:v', '-an', '-c:v', 'libx264', '-preset', 'medium', '-crf', '20', '-vf', res_config['scale'], '-b:v', res_config['br'], '-maxrate', res_config['br'], '-bufsize', str(int(res_config['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']
385
- subprocess.run(cmd_video, capture_output=True, text=True, timeout=7200)
386
- video_streams.append({'file': f"video_{res_config['label']}.m3u8", 'resolution': res_config['res'], 'bandwidth': int(res_config['br'].replace('k', '000')) + 192000, 'codecs': 'avc1.640028'})
387
-
388
- create_master_m3u8(output_dir, video_streams, audio_playlists)
389
-
390
- elif stream_format == "DASH (MPD)":
391
- msg, logs = log_generator("🎬 Generando DASH (fMP4)...", logs)
392
- yield msg, logs, ""
393
- cmd = ['ffmpeg', '-i', source_val]
394
- is_copy_mode = (conversion_option == "Opción 1: Copy Video + Copy Audio")
395
- audio_codec = 'copy' if is_copy_mode else 'aac'
396
- video_codec = 'copy' if is_copy_mode else 'libx264'
397
 
398
- if is_copy_mode or conversion_option == "Opción 2":
399
- cmd.extend(['-map', '0:v:0', '-c:v:0', video_codec])
400
  else:
401
- res_list = []
402
- if conversion_option == "Opción 3: Multi-Res (1080p + 720p) H.264":
403
- res_list = [{'idx': 0, 'scale': 'scale=-2:1080', 'br': '5000k'}, {'idx': 1, 'scale': 'scale=-2:720', 'br': '2800k'}]
404
- elif conversion_option == "Opción 4: Multi-Res (4K + 1080p + 720p) H.264":
405
- res_list = [{'idx': 0, 'scale': 'scale=-2:2160', 'br': '15000k'}, {'idx': 1, 'scale': 'scale=-2:1080', 'br': '5000k'}, {'idx': 2, 'scale': 'scale=-2:720', 'br': '2800k'}]
406
- for r in res_list:
407
- cmd.extend(['-map', '0:v:0'])
408
- cmd.extend([f'-c:v:{r["idx"]}', video_codec, '-preset', 'medium', '-crf', '20', f'-vf:v:{r["idx"]}', r['scale'], f'-b:v:{r["idx"]}', r['br'], '-pix_fmt', 'yuv420p'])
409
-
410
- for i, track in enumerate(audio_tracks):
411
- cmd.extend(['-map', f"0:{track['index']}", f'-c:a:{i}', audio_codec, '-b:a:192k', '-ar', '48000'])
412
-
413
- mpd_output = output_dir / "manifest.mpd"
414
- 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'])
415
- subprocess.run(cmd, capture_output=True, text=True, timeout=7200)
416
-
417
- # --- Subida a destinos ---
418
- result_links = [f"**{base_name}**"]
419
 
420
- if upload_codeberg_flag:
421
- for update in upload_to_codeberg(output_dir, repo_name, codeberg_token, username, int(batch_size_vid), stream_format, logs):
422
- yield update[0], update[1], "\n\n".join(final_links)
423
- if update[2]:
424
- result_links.append(f"📂 Codeberg: {update[2]}")
 
 
 
 
425
 
426
- if upload_cloudflare_flag:
427
- for update in upload_to_cloudflare_pages(output_dir, repo_name, cf_token, cf_account_id, stream_format, logs):
428
- yield update[0], update[1], "\n\n".join(final_links)
429
- if update[2]:
430
- result_links.append(f"☁️ Cloudflare: {update[2]}")
 
 
 
 
 
 
 
431
 
432
- final_links.append("\n".join(result_links))
 
 
 
 
433
 
434
- if delete_local:
435
- try:
436
- shutil.rmtree(output_dir)
437
- msg, logs = log_generator("🗑️ Archivos locales eliminados", logs)
438
- yield msg, logs, "\n\n".join(final_links)
439
- except:
440
- pass
441
-
442
- except Exception as global_e:
443
- import traceback
444
- logs.append(f"\n❌ ERROR: {str(global_e)}")
445
- print(traceback.format_exc())
446
- yield "\n".join(logs), logs, "\n\n".join(final_links)
447
-
448
- msg, logs = log_generator("\n✅ Proceso completado", logs)
449
- yield msg, logs, "\n\n".join(final_links)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
 
451
- # --- UI LIMPIA ESTILO HUGGING FACE ---
452
 
453
  with gr.Blocks(title="Video Streaming Converter", theme=gr.themes.Soft()) as demo:
454
 
455
  gr.Markdown("# 🎬 Video Streaming Converter")
456
- gr.Markdown("Convierte videos a HLS/DASH y súbelos automáticamente a Codeberg o Cloudflare Pages")
457
 
458
  with gr.Row():
459
  with gr.Column(scale=1):
460
- # Entrada
461
- with gr.Tab("Archivo"):
462
- input_file = gr.File(
463
- label="Subir video(s)",
464
- file_count="multiple",
465
- file_types=["video"]
466
- )
467
- with gr.Tab("URL"):
468
- input_urls = gr.Textbox(
469
- label="URLs de videos (opcional, una por línea)",
470
- lines=3,
471
- placeholder="https://ejemplo.com/video.mp4"
472
- )
 
 
 
 
 
473
 
474
- # Configuración
475
- gr.Markdown("### ⚙️ Configuración")
476
- conversion_option = gr.Dropdown(
477
- choices=[
478
- "Opción 1: Copy Video + Copy Audio",
479
- "Opción 2: Copy Video + MP3 Audio",
480
- "Opción 3: Multi-Res (1080p + 720p) H.264",
481
- "Opción 4: Multi-Res (4K + 1080p + 720p) H.264"
482
- ],
483
- value="Opción 3: Multi-Res (1080p + 720p) H.264",
484
- label="Modo de conversión"
485
  )
 
486
  stream_format = gr.Radio(
487
- choices=["HLS (M3U8)", "DASH (MPD)"],
488
- value="HLS (M3U8)",
489
- label="Formato de streaming"
490
  )
491
 
492
  # Destino
493
- gr.Markdown("### 🌐 Destino de subida")
494
- upload_codeberg = gr.Checkbox(value=True, label="Subir a Codeberg")
495
- upload_cloudflare = gr.Checkbox(value=False, label="Subir a Cloudflare Pages")
496
-
497
- # Credenciales
498
- with gr.Accordion("🔑 Credenciales (API Tokens)", open=False):
499
- codeberg_token = gr.Textbox(
500
- label="Codeberg Token",
501
- type="password",
502
- placeholder="Token con permisos 'repo'"
503
- )
504
- cf_token = gr.Textbox(
505
- label="Cloudflare API Token",
506
- type="password",
507
- placeholder="Token con permisos 'Cloudflare Pages'"
508
- )
509
- cf_account_id = gr.Textbox(
510
- label="Cloudflare Account ID",
511
- placeholder="ID de tu cuenta Cloudflare"
512
- )
513
 
514
  # Opciones avanzadas
515
- with gr.Accordion("⚙️ Opciones avanzadas", open=False):
516
- batch_size = gr.Number(
517
- value=20,
518
- label="Tamaño de lote (Git)",
519
- precision=0,
520
- minimum=10,
521
- maximum=100
522
- )
523
- delete_local = gr.Checkbox(
524
- value=True,
525
- label="Eliminar archivos locales al finalizar"
526
- )
527
-
528
- btn = gr.Button("🚀 PROCESAR Y SUBIR", variant="primary", size="lg")
529
 
530
  with gr.Column(scale=2):
531
- # Logs
532
- gr.Markdown("### 🖥️ Logs de proceso")
533
- log_output = gr.Textbox(
534
- lines=20,
535
- interactive=False,
536
- show_label=False,
537
- placeholder="Los logs aparecerán aquí durante el proceso..."
538
- )
539
 
540
- # Enlaces
541
- gr.Markdown("### 🔗 Enlaces generados")
542
- final_links = gr.Textbox(
543
- lines=8,
544
- interactive=False,
545
- show_label=False,
546
- placeholder="Los enlaces de streaming aparecerán aquí al finalizar..."
547
- )
548
 
549
  gr.Markdown("---")
550
- gr.Markdown("💡 **Tip:** Marca solo el destino que necesites. Cloudflare Pages es más rápido pero público, Codeberg es privado pero más lento.")
551
 
552
  # Eventos
553
- btn.click(
554
  fn=process_video,
555
  inputs=[
556
- codeberg_token, cf_token, cf_account_id,
557
- input_file, input_urls,
558
- conversion_option, stream_format,
559
- batch_size, delete_local,
560
- upload_codeberg, upload_cloudflare
561
  ],
562
- outputs=[log_output, gr.State(), final_links]
563
  )
564
 
565
  if __name__ == "__main__":
566
- demo.launch(
567
- server_name="0.0.0.0",
568
- server_port=7860,
569
- share=False,
570
- show_error=True,
571
- max_file_size=2_000_000_000
572
- )
 
11
  try:
12
  import requests
13
  except ImportError:
14
+ print("Error: Falta instalar 'requests'")
15
 
16
  # --- Configuración Global ---
17
  subprocess.run(["git", "config", "--global", "user.email", "bot@codeberg.org"], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
 
91
  # --- Funciones de Subida ---
92
 
93
  def upload_to_codeberg(output_dir, repo_name, codeberg_token, username, batch_size, stream_format, logs):
 
94
  try:
95
  msg, logs = log_generator("📦 Creando repositorio en Codeberg...", logs)
96
  yield msg, logs, None
 
101
 
102
  try:
103
  r = requests.post(url_api, headers=headers, json=data, timeout=30)
104
+ except: pass
 
 
 
 
 
105
 
106
  repo_url = f"https://codeberg.org/{username}/{repo_name}"
107
  git_dir = str(output_dir)
 
124
  if manifest_files:
125
  subprocess.run(['git', 'add'] + manifest_files, cwd=git_dir, check=True)
126
  subprocess.run(['git', 'commit', '-m', 'Add manifests'], cwd=git_dir, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
127
+ try:
128
+ subprocess.run(['git', 'push', '-f', 'origin', 'HEAD:main'], cwd=git_dir, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=600)
129
+ except: pass
130
 
131
+ msg, logs = log_generator(f"⬆️ Subiendo {len(seg_files)} segmentos...", logs)
132
  yield msg, logs, None
133
 
134
  for i in range(0, len(seg_files), batch_size):
135
  batch = seg_files[i:i+batch_size]
136
  subprocess.run(['git', 'add'] + batch, cwd=git_dir, check=True)
137
  subprocess.run(['git', 'commit', '-m', f'Batch {i}'], cwd=git_dir, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
138
+ try:
139
+ subprocess.run(['git', 'push', '-f', 'origin', 'HEAD:main'], cwd=git_dir, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=600)
140
+ except: pass
141
 
142
  final_url = f"{repo_url}/raw/branch/main/{manifest_name}"
143
  msg, logs = log_generator(f"✅ Codeberg: {final_url}", logs)
 
148
  yield msg, logs, None
149
 
150
  def upload_to_cloudflare_pages(output_dir, project_name, cf_token, cf_account_id, stream_format, logs):
 
151
  try:
152
+ msg, logs = log_generator("☁️ Subiendo a Cloudflare Pages...", logs)
153
  yield msg, logs, None
154
 
155
+ headers = {"Authorization": f"Bearer {cf_token}"}
 
 
 
 
156
  project_url = f"https://api.cloudflare.com/client/v4/accounts/{cf_account_id}/pages/projects"
157
 
158
  try:
 
160
  if r.status_code == 200:
161
  projects = r.json().get('result', [])
162
  exists = any(p['name'] == project_name for p in projects)
 
163
  if not exists:
164
+ project_data = {"name": project_name, "production_branch": "main"}
165
+ requests.post(project_url, headers=headers, json=project_data, timeout=30)
166
+ except: pass
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
  zip_path = output_dir.parent / f"{project_name}.zip"
169
  with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
 
171
  if file.is_file():
172
  zipf.write(file, file.name)
173
 
 
 
 
174
  deploy_url = f"{project_url}/{project_name}/deployments"
175
 
176
  with open(zip_path, 'rb') as f:
177
  files_upload = {'file': (f'{project_name}.zip', f, 'application/zip')}
178
+ r = requests.post(deploy_url, headers={"Authorization": f"Bearer {cf_token}"}, files=files_upload, timeout=300)
 
 
179
 
180
  if r.status_code in [200, 201]:
181
  result = r.json().get('result', {})
182
  cf_url = result.get('url', f"https://{project_name}.pages.dev")
183
  manifest_name = "manifest.mpd" if stream_format == "DASH (MPD)" else "master.m3u8"
184
  final_url = f"{cf_url}/{manifest_name}"
185
+ msg, logs = log_generator(f"✅ Cloudflare: {final_url}", logs)
186
  yield msg, logs, final_url
 
187
  zip_path.unlink()
188
  else:
189
+ raise Exception(f"Error: {r.status_code}")
190
 
191
  except Exception as e:
192
  msg, logs = log_generator(f"❌ Error Cloudflare: {str(e)}", logs)
193
  yield msg, logs, None
194
 
195
+ # --- Procesamiento Principal ---
196
 
197
+ def process_video(file_input, url_input, codeberg_token, cf_token, cf_account_id,
198
+ conversion_mode, stream_format, upload_codeberg_flag, upload_cloudflare_flag,
199
+ batch_size, delete_local, progress=gr.Progress()):
 
 
 
 
 
 
 
 
 
200
 
201
+ logs = ["🚀 Iniciando conversión..."]
 
 
 
202
 
203
+ try:
204
+ # Validar entrada
205
+ source = None
206
+ is_url = False
207
+
208
+ if file_input:
209
+ source = file_input.name
210
+ elif url_input:
211
+ source = url_input
212
+ is_url = True
213
+ else:
214
+ return "\n".join(logs + ["❌ Selecciona archivo o URL"]), "Error"
215
+
216
+ # Validar destinos
217
+ if not upload_codeberg_flag and not upload_cloudflare_flag:
218
+ return "\n".join(logs + ["❌ Selecciona al menos un destino"]), "Error"
219
+
220
+ if upload_codeberg_flag and not codeberg_token:
221
+ return "\n".join(logs + ["❌ Se requiere Token de Codeberg"]), "Error"
222
+
223
+ if upload_cloudflare_flag and (not cf_token or not cf_account_id):
224
+ return "\n".join(logs + ["❌ Se requiere Token y Account ID de Cloudflare"]), "Error"
225
+
226
+ # Validar usuario Codeberg
227
+ username = None
228
+ if upload_codeberg_flag:
229
+ headers_cb = {"Authorization": f"token {codeberg_token}"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  try:
231
+ user_resp = requests.get("https://codeberg.org/api/v1/user", headers=headers_cb, timeout=10)
232
+ if user_resp.status_code != 200:
233
+ raise Exception("Token de Codeberg inválido")
234
+ username = user_resp.json().get('login')
235
+ logs.append(f"👤 Usuario: {username}")
236
+ except Exception as e:
237
+ return "\n".join(logs + [f"❌ {str(e)}"]), "Error"
238
+
239
+ # Preparar nombres
240
+ base_name = Path(source).stem if not is_url else get_filename_from_url(source)
241
+ repo_name = clean_for_repo(base_name)
242
+ folder_name = clean_for_folder(base_name) + f"_{stream_format.lower().replace(' ', '_')}"
243
+ output_dir = Path.cwd() / folder_name
244
+
245
+ if output_dir.exists():
246
+ shutil.rmtree(output_dir)
247
+ output_dir.mkdir(exist_ok=True)
248
+
249
+ logs.append(f"📹 Procesando: {base_name}")
250
+ yield "\n".join(logs), "Procesando"
251
+
252
+ # Detectar audio
253
+ audio_tracks = detect_audio_streams(source)
254
+ if not audio_tracks:
255
+ audio_tracks = [{'index': 0, 'language': 'und', 'title': 'Audio', 'codec': 'unknown'}]
256
+ audio_tracks = prioritize_audio_tracks(audio_tracks)
257
+
258
+ logs.append(f"🎵 {len(audio_tracks)} streams de audio detectados")
259
+ yield "\n".join(logs), "Procesando"
260
+
261
+ # Conversión FFmpeg
262
+ if stream_format == "HLS (M3U8)":
263
+ # Procesar audio
264
+ audio_playlists = []
265
+ for i, track in enumerate(audio_tracks):
266
+ audio_file = output_dir / f"audio_{i}.m3u8"
267
+ seg_audio = output_dir / f"audio_{i}_%03d.ts"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
 
269
+ if conversion_mode == "Copy Video + Copy Audio":
270
+ audio_params = ['-c:a', 'copy']
271
  else:
272
+ audio_params = ['-c:a', 'libmp3lame', '-b:a', '192k', '-ar', '48000']
273
+
274
+ cmd_audio = ['ffmpeg', '-i', source, '-map', f"0:{track['index']}"] + audio_params + [
275
+ '-hls_time', '10', '-hls_list_size', '0', '-hls_segment_filename', str(seg_audio), str(audio_file), '-y', '-loglevel', 'warning'
276
+ ]
277
+ subprocess.run(cmd_audio, capture_output=True, timeout=3600)
278
+ audio_playlists.append({'file': f"audio_{i}.m3u8", 'language': track['language'], 'title': track['title'], 'is_default': i == 0})
 
 
 
 
 
 
 
 
 
 
 
279
 
280
+ # Procesar video
281
+ video_streams = []
282
+ if conversion_mode in ["Copy Video + Copy Audio", "Copy Video + MP3 Audio"]:
283
+ video_file = output_dir / "video_1080p.m3u8"
284
+ seg_video = output_dir / "video_1080p_%03d.ts"
285
+ cmd_video = ['ffmpeg', '-i', source, '-map', '0:v', '-c:v', 'copy', '-an', '-hls_time', '10', '-hls_list_size', '0',
286
+ '-hls_segment_filename', str(seg_video), str(video_file), '-y', '-loglevel', 'warning']
287
+ subprocess.run(cmd_video, capture_output=True, timeout=7200)
288
+ video_streams.append({'file': 'video_1080p.m3u8', 'resolution': '1920x1080', 'bandwidth': 5000000, 'codecs': 'avc1.640028'})
289
 
290
+ elif conversion_mode == "Multi-Res (1080p + 720p)":
291
+ for res in [{'label': '1080p', 'scale': 'scale=-2:1080', 'br': '5000k', 'res': '1920x1080'},
292
+ {'label': '720p', 'scale': 'scale=-2:720', 'br': '2800k', 'res': '1280x720'}]:
293
+ video_file = output_dir / f"video_{res['label']}.m3u8"
294
+ seg_video = output_dir / f"video_{res['label']}_%03d.ts"
295
+ cmd_video = ['ffmpeg', '-i', source, '-map', '0:v', '-an', '-c:v', 'libx264', '-preset', 'medium', '-crf', '20',
296
+ '-vf', res['scale'], '-b:v', res['br'], '-maxrate', res['br'], '-bufsize', str(int(res['br'].replace('k',''))*2) + 'k',
297
+ '-pix_fmt', 'yuv420p', '-hls_time', '10', '-hls_list_size', '0', '-hls_segment_filename', str(seg_video),
298
+ str(video_file), '-y', '-loglevel', 'warning']
299
+ subprocess.run(cmd_video, capture_output=True, timeout=7200)
300
+ video_streams.append({'file': f"video_{res['label']}.m3u8", 'resolution': res['res'],
301
+ 'bandwidth': int(res['br'].replace('k', '000')) + 192000, 'codecs': 'avc1.640028'})
302
 
303
+ create_master_m3u8(output_dir, video_streams, audio_playlists)
304
+
305
+ elif stream_format == "DASH (MPD)":
306
+ cmd = ['ffmpeg', '-i', source]
307
+ is_copy = (conversion_mode == "Copy Video + Copy Audio")
308
 
309
+ if is_copy:
310
+ cmd.extend(['-map', '0:v:0', '-c:v:0', 'copy'])
311
+ else:
312
+ if conversion_mode == "Multi-Res (1080p + 720p)":
313
+ for idx, r in enumerate([{'scale': 'scale=-2:1080', 'br': '5000k'}, {'scale': 'scale=-2:720', 'br': '2800k'}]):
314
+ cmd.extend(['-map', '0:v:0', f'-c:v:{idx}', 'libx264', '-preset', 'medium', '-crf', '20',
315
+ f'-vf:v:{idx}', r['scale'], f'-b:v:{idx}', r['br'], '-pix_fmt', 'yuv420p'])
316
+
317
+ for i, track in enumerate(audio_tracks):
318
+ cmd.extend(['-map', f"0:{track['index']}", f'-c:a:{i}', 'aac' if not is_copy else 'copy', '-b:a:192k', '-ar', '48000'])
319
+
320
+ mpd_output = output_dir / "manifest.mpd"
321
+ cmd.extend(['-f', 'dash', '-seg_duration', '10', '-use_template', '1', '-use_timeline', '0',
322
+ '-init_seg_name', 'init-$RepresentationID$.m4s', '-media_seg_name', 'chunk-$RepresentationID$-$Number%05d$.m4s',
323
+ str(mpd_output), '-y', '-loglevel', 'warning'])
324
+ subprocess.run(cmd, capture_output=True, timeout=7200)
325
+
326
+ logs.append("✅ Conversión completada")
327
+ yield "\n".join(logs), "Subiendo"
328
+
329
+ # Subir a destinos
330
+ result_links = []
331
+
332
+ if upload_codeberg_flag:
333
+ for update in upload_to_codeberg(output_dir, repo_name, codeberg_token, username, int(batch_size), stream_format, logs):
334
+ yield update[0], "Subiendo"
335
+ if update[2]:
336
+ result_links.append(f"📂 Codeberg: {update[2]}")
337
+
338
+ if upload_cloudflare_flag:
339
+ for update in upload_to_cloudflare_pages(output_dir, repo_name, cf_token, cf_account_id, stream_format, logs):
340
+ yield update[0], "Subiendo"
341
+ if update[2]:
342
+ result_links.append(f"☁️ Cloudflare: {update[2]}")
343
+
344
+ if delete_local:
345
+ shutil.rmtree(output_dir)
346
+ logs.append("🗑️ Archivos locales eliminados")
347
+
348
+ final_output = "\n".join(logs + ["", "🔗 Enlaces:"] + result_links)
349
+ return final_output, "¡Listo!"
350
+
351
+ except Exception as e:
352
+ import traceback
353
+ traceback.print_exc()
354
+ return "\n".join(logs + [f"❌ ERROR: {str(e)}"]), "Fallo"
355
 
356
+ # --- Interfaz Gradio ---
357
 
358
  with gr.Blocks(title="Video Streaming Converter", theme=gr.themes.Soft()) as demo:
359
 
360
  gr.Markdown("# 🎬 Video Streaming Converter")
361
+ gr.Markdown("Convierte videos a HLS/DASH y súbelos a Codeberg o Cloudflare Pages")
362
 
363
  with gr.Row():
364
  with gr.Column(scale=1):
365
+ # Tokens
366
+ codeberg_token = gr.Textbox(
367
+ label="Codeberg Token",
368
+ value="92427ac14a228f0762ec303d478b9f093be4f608",
369
+ type="password",
370
+ placeholder="Token con permisos 'repo'"
371
+ )
372
+ cf_token = gr.Textbox(
373
+ label="Cloudflare Token",
374
+ value="mOvchd-yxYyQ6Zj3xMb_38Rkf-HwROchlsx-Ud9H",
375
+ type="password",
376
+ placeholder="Token de Cloudflare Pages"
377
+ )
378
+ cf_account_id = gr.Textbox(
379
+ label="Cloudflare Account ID",
380
+ value="bd06ac4017668e45b656db342029929d",
381
+ placeholder="ID de tu cuenta"
382
+ )
383
 
384
+ # Modo
385
+ conversion_mode = gr.Radio(
386
+ choices=["Copy Video + Copy Audio", "Copy Video + MP3 Audio", "Multi-Res (1080p + 720p)"],
387
+ value="Multi-Res (1080p + 720p)",
388
+ label="Modo"
 
 
 
 
 
 
389
  )
390
+
391
  stream_format = gr.Radio(
392
+ choices=["HLS (M3U8)", "DASH (MPD)"],
393
+ value="HLS (M3U8)",
394
+ label="Formato"
395
  )
396
 
397
  # Destino
398
+ upload_codeberg = gr.Checkbox(label="Subir a Codeberg", value=True)
399
+ upload_cloudflare = gr.Checkbox(label="Subir a Cloudflare Pages", value=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
 
401
  # Opciones avanzadas
402
+ with gr.Accordion("Opciones Avanzadas", open=False):
403
+ batch_size = gr.Number(value=20, label="Batch Size", precision=0)
404
+ delete_local = gr.Checkbox(value=True, label="Borrar archivos locales")
 
 
 
 
 
 
 
 
 
 
 
405
 
406
  with gr.Column(scale=2):
407
+ # Entrada
408
+ with gr.Tab("Archivo"):
409
+ file_input = gr.File(label="Subir Video", file_types=["video"])
410
+ with gr.Tab("URL"):
411
+ url_input = gr.Textbox(label="URL del Video", placeholder="https://ejemplo.com/video.mp4")
 
 
 
412
 
413
+ # Botón
414
+ btn_process = gr.Button("🚀 PROCESAR Y SUBIR", variant="primary", size="lg")
415
+
416
+ # Outputs
417
+ log_output = gr.Textbox(label="Log", lines=15, interactive=False)
418
+ status_output = gr.Textbox(label="Estado", interactive=False)
 
 
419
 
420
  gr.Markdown("---")
421
+ gr.Markdown("💡 **Tip:** Cloudflare Pages es rápido pero público. Codeberg es privado pero más lento.")
422
 
423
  # Eventos
424
+ btn_process.click(
425
  fn=process_video,
426
  inputs=[
427
+ file_input, url_input, codeberg_token, cf_token, cf_account_id,
428
+ conversion_mode, stream_format, upload_codeberg, upload_cloudflare,
429
+ batch_size, delete_local
 
 
430
  ],
431
+ outputs=[log_output, status_output]
432
  )
433
 
434
  if __name__ == "__main__":
435
+ demo.launch()