import os, re, time, asyncio, threading, httpx from fastapi import FastAPI, Request, BackgroundTasks from fastapi.responses import HTMLResponse, JSONResponse from fastapi.middleware.cors import CORSMiddleware app = FastAPI() @app.on_event("startup") async def iniciar_pings(): import asyncio, httpx async def ping_loop(): while True: await asyncio.sleep(21600) # 6 horas try: async with httpx.AsyncClient(timeout=15) as client: for email, datos in list(CLIENTES.items()): url = datos.get("url", "") if url: try: await client.get(url) print(f"Ping OK: {url}", flush=True) except Exception as e: print(f"Ping fail {url}: {e}", flush=True) except Exception as ex: print(f"Error ping loop: {ex}", flush=True) asyncio.create_task(ping_loop()) app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) HF_USER = os.environ.get("HF_USER", "Cristobal299") HF_TOKEN = os.environ.get("HF_TOKEN", "") GROQ_KEY = os.environ.get("GROQ_KEY", "") PEXELS_KEY = os.environ.get("PEXELS_KEY", "") STRIPE_KEY = os.environ.get("STRIPE_KEY", "") STRIPE_WEBHOOK_SECRET = os.environ.get("STRIPE_WEBHOOK_SECRET", "") TOKENS_VALIDOS = {} @app.post("/stripe-webhook") async def stripe_webhook(request: Request): import hmac, hashlib, secrets payload = await request.body() sig = request.headers.get("stripe-signature","") try: elements = {k:v for k,v in [x.split("=",1) for x in sig.split(",")]} ts = elements.get("t","") expected = hmac.new(STRIPE_WEBHOOK_SECRET.encode(), f"{ts}.{payload.decode()}".encode(), hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, elements.get("v1","")): return JSONResponse({"error":"firma invalida"}, status_code=400) except: return JSONResponse({"error":"error verificacion"}, status_code=400) import json data = json.loads(payload) if data.get("type") == "checkout.session.completed": email = data["data"]["object"].get("customer_email","") token = secrets.token_urlsafe(32) TOKENS_VALIDOS[token] = email return {"ok": True} RESEND_KEY = os.environ.get("RESEND_KEY", "") def enviar_email(email, nicho, url): try: import requests html = f"""

πŸ€– TubeBot activado

Tu canal de {nicho} ya estΓ‘ funcionando.

Ver mi sistema en tiempo real

Si tienes dudas responde este email.

""" requests.post("https://api.brevo.com/v3/smtp/email", headers={"api-key": os.environ.get("BREVO_KEY",""), "Content-Type": "application/json", "accept": "application/json"}, json={ "sender": {"name": "TubeBot", "email": "dayron100813@gmail.com"}, "to": [{"email": email}], "subject": "Tu canal de YouTube ya esta listo", "htmlContent": html } ) except Exception as e: print(f"Error email: {e}") CLIENTES = {} def _enviar_aviso_real(asunto, mensaje): try: import requests as _r html = mensaje.replace("", "").replace("", "").replace("\n", "
") _r.post("https://api.brevo.com/v3/smtp/email", headers={"api-key": os.environ.get("BREVO_KEY",""), "Content-Type": "application/json", "accept": "application/json"}, json={ "sender": {"name": "TubeBot Avisos", "email": "dayron100813@gmail.com"}, "to": [{"email": "Sicariodedios69@gmail.com"}], "subject": asunto, "htmlContent": f"
{html}
" }, timeout=30 ) except Exception as e: print(f"Error aviso: {e}", flush=True) def avisar_telegram(mensaje): """Manda aviso por Gmail (Brevo) en thread aparte""" import threading asunto_corto = mensaje.split("\n")[0].replace("", "").replace("", "").strip() threading.Thread(target=_enviar_aviso_real, args=(asunto_corto[:100], mensaje), daemon=True).start() def cargar_clientes(): global CLIENTES try: import json r = __import__("requests").get( f"https://huggingface.co/api/repos/Cristobal299/IAweb/raw/main/clientes.json", headers={"Authorization": f"Bearer {HF_TOKEN}"} ) if r.status_code == 200: CLIENTES = r.json() except: pass def guardar_clientes(): try: import json data = json.dumps(CLIENTES).encode() __import__("requests").put( f"https://huggingface.co/api/repos/Cristobal299/IAweb/upload/main/clientes.json", headers={"Authorization": f"Bearer {HF_TOKEN}", "Content-Type": "application/octet-stream"}, data=data ) except Exception as e: print(f"Error guardando clientes: {e}") cargar_clientes() # ══════════════════════════════════════════ # CREAR SPACE EN HUGGING FACE # ══════════════════════════════════════════ def token_de_repo(repo): """Del repo (ej Maria288/tb-xyz) saca el usuario y devuelve su token HF""" usuario = repo.split("/")[0] if "/" in repo else "" mapa = { os.environ.get("HF_USER_5","Alejandro"): os.environ.get("HF_TOKEN_5",""), os.environ.get("HF_USER_4","Maria288"): os.environ.get("HF_TOKEN_4",""), os.environ.get("HF_USER_2","Cristobal2999"): os.environ.get("HF_TOKEN_2",""), os.environ.get("HF_USER_3","Cristobal288"): os.environ.get("HF_TOKEN_3",""), } return mapa.get(usuario, "") async def crear_space(nicho: str, email: str, tipo: str = "largo", config_parsed: dict = None, archivos_subidos: list = None) -> str: space_id = f"tb-{re.sub(r'[^a-z0-9]','', nicho.lower()[:12])}-{int(time.time())%9999}" cuentas = [ (os.environ.get("HF_TOKEN_5",""), os.environ.get("HF_USER_5","Alejandro")), (os.environ.get("HF_TOKEN_4",""), os.environ.get("HF_USER_4","Maria288")), (os.environ.get("HF_TOKEN_2",""), os.environ.get("HF_USER_2","Cristobal2999")), (os.environ.get("HF_TOKEN_3",""), os.environ.get("HF_USER_3","Cristobal288")), ] cuentas = [(t,u) for t,u in cuentas if t] import random token_actual, hf_user_actual = random.choice(cuentas) print(f"DEBUG user={hf_user_actual} token={token_actual[:8]}", flush=True) headers = {"Authorization": f"Bearer {token_actual}", "Content-Type": "application/json"} with open("cliente_template.py", "r") as _f: main_code = _f.read().replace("NICHO_PLACEHOLDER", nicho).replace("EMAIL_PLACEHOLDER", email).replace("REPO_PLACEHOLDER", f"{hf_user_actual}/{space_id}").replace("GROQ_PLACEHOLDER", os.environ.get("GROQ_KEY","")).replace("GROQ_PLACEHOLDER_2", os.environ.get("GROQ_KEY_2","")).replace("GROQ_PLACEHOLDER_3", os.environ.get("GROQ_KEY_3","")).replace("PEXELS_PLACEHOLDER", os.environ.get("PEXELS_KEY","")).replace("TIPO_PLACEHOLDER", tipo).replace("CONFIG_PLACEHOLDER", __import__("json").dumps(config_parsed or {})) req = "gradio\ngroq\nedge-tts\nmoviepy==1.0.3\nrequests\nhttpx\ngoogle-api-python-client\ngoogle-auth\ngoogle-auth-oauthlib\nimageio\nimageio-ffmpeg\ndecorator\ntqdm\nPillow\nnumpy\nopencv-python-headless" async with httpx.AsyncClient(timeout=30) as client: # Crear repo r = await client.post("https://huggingface.co/api/repos/create", headers=headers, json={"name": space_id, "type": "space", "sdk": "docker", "private": False}) repo_name = r.json().get("name", f"{hf_user_actual}/{space_id}") if r.status_code not in [200, 201]: print(f"Error repo: {r.text}") return None repo_name = r.json().get("name", f"{hf_user_actual}/{space_id}") print(f"Repo OK: {repo_name}", flush=True) # Subir archivos via commit API import base64, json as _json def b64(s): return base64.b64encode(s.encode("utf-8")).decode("utf-8") readme = """--- title: TubeBot emoji: πŸ€– colorFrom: red colorTo: gray sdk: docker pinned: false ---""" dockerfile = """FROM python:3.10-slim RUN apt-get update && apt-get install -y imagemagick ffmpeg && rm -rf /var/lib/apt/lists/* RUN sed -i "s/rights=\"none\"/rights=\"read|write\"/g" /etc/ImageMagick-7/policy.xml || true WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY main.py . COPY media_* ./ EXPOSE 7860 CMD [\"uvicorn\", \"main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"7860\"]""" commit_payload = { "summary": "TubeBot init", "files": [ {"path": "main.py", "content": b64(main_code), "encoding": "base64"}, {"path": "requirements.txt", "content": b64(req), "encoding": "base64"}, {"path": "README.md", "content": b64(readme), "encoding": "base64"}, {"path": "Dockerfile", "content": b64(dockerfile), "encoding": "base64"} ] } r2 = await client.post( f"https://huggingface.co/api/spaces/{repo_name}/commit/main", headers={"Authorization": f"Bearer {token_actual}", "Content-Type": "application/json"}, content=_json.dumps(commit_payload).encode() ) print(f"Commit: {r2.status_code} {r2.text[:200]}") # Subir archivos del cliente uno a uno (evita problema de binarios) if archivos_subidos: for i, arc in enumerate(archivos_subidos[:20]): try: nombre = arc.get("nombre", f"media_{i}.jpg").replace("/","_").replace("\\","_") contenido_b64 = arc.get("contenido", "") if not contenido_b64: continue binario = base64.b64decode(contenido_b64) commit_arc = { "summary": f"upload {nombre}", "files": [ {"path": f"media_{nombre}", "content": contenido_b64, "encoding": "base64"} ] } # 3 intentos con espera for intento in range(3): r_up = await client.post( f"https://huggingface.co/api/spaces/{repo_name}/commit/main", headers={"Authorization": f"Bearer {token_actual}", "Content-Type": "application/json"}, content=_json.dumps(commit_arc).encode() ) if r_up.status_code in [200, 201]: break await asyncio.sleep(2) print(f"Upload {nombre}: {r_up.status_code}", flush=True) except Exception as e: print(f"Error upload {arc.get('nombre','?')}: {e}", flush=True) # Agregar secretos al Space del cliente secretos = { "GROQ_KEY": os.environ.get("GROQ_KEY",""), "PEXELS_KEY": os.environ.get("PEXELS_KEY",""), "GOOGLE_CLIENT_ID": os.environ.get("GOOGLE_CLIENT_ID",""), "GOOGLE_CLIENT_SECRET": os.environ.get("GOOGLE_CLIENT_SECRET",""), "CLIENT_EMAIL": email, } for key, val in secretos.items(): await client.post( f"https://huggingface.co/api/spaces/{hf_user_actual}/{space_id}/secrets", headers={"Authorization": f"Bearer {token_actual}", "Content-Type": "application/json"}, json={"key": key, "value": val} ) url = f"https://{hf_user_actual.lower()}-{space_id.lower()}.hf.space" return {"url": url, "repo": repo_name} # ══════════════════════════════════════════ # PÁGINA PRINCIPAL β€” VENTAS # ══════════════════════════════════════════ VENTAS_HTML = """ TubeBot β€” Canal de YouTube AutomΓ‘tico
πŸ“’ Publicidad Automatica con IA

TU MARCA
EN TODAS PARTES

Genera contenido en video para tu negocio, marca o proyecto sin grabar, sin editar y sin contratar a nadie.

El sistema crea y publica videos solo. Tu presencia crece mientras tu trabajas en lo que importa.

πŸš€ QUIERO MI SISTEMA Desde $49 Β· Pago unico
El problema

CREAR CONTENIDO
CUESTA CARO

Estar presente hoy significa publicar video constantemente. Pero grabar, editar y publicar cada dia te roba horas o te obliga a pagar a un editor, una agencia o un community manager.

Mientras tanto, tu competencia publica todos los dias y se queda con la atencion de tu cliente. Necesitas estar presente sin que te consuma la vida.

La solucion

UN SISTEMA QUE
TRABAJA POR TI

A quien sirve

ΒΏPARA QUIEN ES?

  • πŸͺ Negocios que quieren mas clientes sin gastar en publicidad cara
  • πŸš€ Emprendedores que venden un producto o servicio
  • 🎨 Creadores y marcas que quieren presencia sin grabar
  • ✍️ Quien quiere estar en todas partes sin perder tiempo
Como funciona

EN 4 PASOS

01
Eliges tu tema y tu mensaje
Le dices de que va tu marca o producto y que quieres comunicar.
02
El sistema crea el contenido
Guion con psicologia de venta, voz de tu pais, imagenes y montaje.
03
El video se produce solo
Composicion profesional con tu material o el del sistema.
04
Se publica en tu canal
Tu presencia crece sola, cada dia, sin que toques nada.
ΒΏPor que TubeBot?

LO QUE TE DA

🎬
Composicion Profesional
Cada imagen se procesa con fondo y composicion cinematografica. Parece hecho por un editor.
πŸŽ™οΈ
Voz de Tu Pais
πŸ‡ͺπŸ‡Έ πŸ‡²πŸ‡½ πŸ‡¦πŸ‡· πŸ‡¨πŸ‡΄ πŸ‡ΊπŸ‡Έ πŸ‡¬πŸ‡§ πŸ‡§πŸ‡· Conecta con tu audiencia local.
πŸ“Έ
Tu Material o el Suyo
Sube tus imagenes y videos, o deja que el sistema los ponga. Tu marca, tu estilo.
🧠
Psicologia de Venta
Los guiones usan gancho, retencion y llamada a la accion. No solo informan, mueven a actuar.
🎨
100% Personalizable
Color de subtitulos, frases, link en descripcion, tono. Todo a tu gusto sin tocar codigo.
πŸ€–
Totalmente Automatico
Una vez activado, publica solo cada 4 horas. Tu presencia crece mientras duermes.
πŸ›‘οΈ

GARANTIA DE 7 DIAS

Si el sistema no funciona como prometo, te devuelvo el 100%. Sin preguntas.

Planes

ELIGE TU PLAN

Pago unico. Sin mensualidades. Acceso de por vida.

Basico
$49
Pago unico
  • βœ… 1 canal automatico
  • βœ… 1 tema a elegir
  • βœ… Videos cada 4 horas
  • βœ… Soporte por email
ELEGIR BASICO
POPULAR
πŸ₯‡ Oro
$147
Pago unico
  • βœ… 5 canales automaticos
  • βœ… 5 temas diferentes
  • βœ… Videos cada 4 horas
  • βœ… Soporte WhatsApp prioritario
  • βœ… Cambio de tema gratis
ELEGIR ORO
πŸ’Ž Diamante
$297
Pago unico
  • βœ… 9 canales automaticos
  • βœ… Todos los temas
  • βœ… Videos cada 4 horas
  • βœ… Soporte VIP 24/7
  • βœ… Temas premium exclusivos
ELEGIR DIAMANTE
πŸ† Platinum
$497
Pago unico
  • βœ… 20 canales automaticos
  • βœ… Todos los temas + futuros
  • βœ… Videos cada 4 horas
  • βœ… WhatsApp directo conmigo
  • βœ… Funciones nuevas antes que nadie
ELEGIR PLATINUM
FAQ

PREGUNTAS FRECUENTES

ΒΏNecesito mostrar mi cara? β–Ό
No. El sistema crea todo el contenido sin que aparezcas.
ΒΏPuedo usar mis propias imagenes y mi marca? β–Ό
Si. Puedes subir tu material o dejar que el sistema lo ponga. Tu mensaje y tu identidad.
ΒΏNecesito experiencia tecnica? β–Ό
No. Eliges tu tema y el sistema hace el resto solo.
ΒΏSirve para promocionar mi negocio? β–Ό
Si. Esa es justo su fuerza: presencia y publicidad automatica para tu marca o producto.
ΒΏHay pagos mensuales? β–Ό
No. Es un pago unico con acceso de por vida.

TU MARCA
NO PUEDE ESPERAR

Cada dia sin publicar es atencion que se lleva otro. Activa tu sistema y empieza a estar presente hoy.

πŸš€ QUIERO MI SISTEMA
""" # ══════════════════════════════════════════ # PÁGINA DE ACTIVACIΓ“N β€” DESPUΓ‰S DEL PAGO # ══════════════════════════════════════════ ACTIVAR_HTML = """ TubeBot β€” Activa tu canal
βœ…

Β‘PAGO CONFIRMADO!

Plan: PLAN_PLACEHOLDER Β· Puedes crear CANALES_PLACEHOLDER canal(es)

⚑ Creando tu sistema en Hugging Face...
Esto puede tardar 1-2 minutos

πŸŽ‰ Β‘Tu canal estΓ‘ listo!

Accede aquΓ­ para ver tu sistema funcionando en tiempo real:

πŸ“Ί Conectar mi canal de YouTube

πŸ“‹ GUÍA PASO A PASO:

1️⃣ Entra a tu URL β€” Si pide login pulsa "Sign in as guest" o "Opciones avanzadas" para entrar sin cuenta
2️⃣ Espera 2-3 minutos β€” La primera vez tarda en arrancar, es normal
3️⃣ VerΓ‘s los logs β€” Pantalla negra con texto dorado mostrando el bot en tiempo real
4️⃣ El bot trabaja solo β€” Genera guiΓ³n, voz, vΓ­deo y lo sube a YouTube cada 4 horas
5️⃣ Sin tocar nada β€” Funciona 24/7 completamente solo
6️⃣ ΒΏProblemas? β€” EscrΓ­beme y te ayudo en minutos

πŸ’¬ Soporte WhatsApp
""" # ══════════════════════════════════════════ # RUTAS # ══════════════════════════════════════════ VISITAS_CACHE = {} # IP -> timestamp para no spamear avisos @app.get("/", response_class=HTMLResponse) async def home(request: Request): try: ip = request.client.host if request.client else "?" import time as _t ahora = _t.time() # Avisar solo si esa IP no visito en los ultimos 10 min if ip not in VISITAS_CACHE or ahora - VISITAS_CACHE[ip] > 600: VISITAS_CACHE[ip] = ahora ua = request.headers.get("user-agent","?")[:80] # Filtrar bots HF y crawlers ignorar = ["HuggingFace", "bot", "spider", "crawler", "python-requests", "curl"] if any(b.lower() in ua.lower() for b in ignorar): pass # No avisar else: referer = request.headers.get("referer", "directo/sin referer") avisar_telegram(f"πŸ‘€ Visita a landing\n🌐 IP: {ip}\nπŸ“± {ua}\nπŸ”— Viene de: {referer}") except: pass return VENTAS_HTML PLANES_CANALES = {"basico": 1, "oro": 5, "diamante": 9, "platinum": 20} @app.get("/activar-canal", response_class=HTMLResponse) async def activar_canal(token: str = "", plan: str = ""): if plan and plan in PLANES_CANALES: import secrets nuevo_token = secrets.token_urlsafe(32) TOKENS_VALIDOS[nuevo_token] = {"plan": plan} avisar_telegram(f"πŸ’° POSIBLE VENTA\nπŸ“¦ Plan: {plan.upper()}\nπŸ”₯ Cliente llego a activar-canal tras pago Stripe") return ACTIVAR_HTML.replace("PLAN_PLACEHOLDER", plan).replace("TOKEN_PLACEHOLDER", nuevo_token).replace("CANALES_PLACEHOLDER", str(PLANES_CANALES[plan])) if token and token in TOKENS_VALIDOS: plan_token = TOKENS_VALIDOS[token].get("plan", "basico") if isinstance(TOKENS_VALIDOS[token], dict) else "basico" return ACTIVAR_HTML.replace("PLAN_PLACEHOLDER", plan_token).replace("TOKEN_PLACEHOLDER", token).replace("CANALES_PLACEHOLDER", str(PLANES_CANALES.get(plan_token, 1))) return HTMLResponse("

Acceso no autorizado

Volver", status_code=403) async def parsear_config_groq(texto): """Parsea instrucciones del cliente con Groq a JSON de configuracion""" if not texto or not texto.strip(): return {} try: import httpx prompt = f"""Analiza las preferencias del cliente y devuelve SOLO un JSON valido con estas keys (omite las que no se mencionen): - voz_ia: true/false (si quiere voz IA, default true si no dice nada) - voz_genero: "masculina" o "femenina" (default masculina) - pais_voz: pais del acento de voz si lo menciona el cliente. Valores: "espaΓ±a", "mexico", "argentina", "colombia", "usa", "uk", "australia", "brasil", "francia". Si no menciona pais, dejar vacio - idioma: "es"/"en"/"pt"/"fr" (default es) - velocidad_voz: 0.8 a 1.5 (default 1.0) - subir_musica: true/false (default false) - subir_voz: true/false (default false) - tono: "serio"/"divertido"/"motivacional"/"educativo" (default educativo) - subtitulos: true/false (default true) - estilo_subtitulos: "tiktok"/"clasico"/"minimal" (default tiktok) - color_subtitulos: codigo hex como "#D4AF37" (default #D4AF37) - posicion_subtitulos: "arriba"/"centro"/"abajo" (default abajo) - frase_inicio: texto literal que el cliente quiere al inicio del video (string vacio si no menciona) - frase_final: texto literal que el cliente quiere al final del video (string vacio si no menciona) - canal_promocionar: URL del canal que quiere promocionar (string vacio si no hay) - link_descripcion: URL/link a poner en descripcion del video (string vacio si no hay) Preferencias del cliente: {texto} Responde SOLO el JSON, sin explicaciones, sin markdown.""" async with httpx.AsyncClient(timeout=20) as cli: r = await cli.post("https://api.groq.com/openai/v1/chat/completions", headers={"Authorization": f"Bearer {os.environ.get('GROQ_KEY','')}"}, json={"model":"llama-3.3-70b-versatile","messages":[{"role":"user","content":prompt}],"temperature":0.2} ) txt = r.json()["choices"][0]["message"]["content"].strip() # Limpiar markdown si lo trae if "```" in txt: txt = txt.split("```")[1].replace("json","",1).strip() import json return json.loads(txt) except Exception as e: print(f"Error parseando config: {e}", flush=True) return {} @app.post("/crear") async def crear(request: Request): data = await request.json() email = data.get("email","").strip().lower() canales_data = data.get("canales", []) archivos_subidos = data.get("archivos", []) if canales_data: nicho = canales_data[0].get("nicho","tecnologia") tipo = canales_data[0].get("tipo","largo") config_libre = canales_data[0].get("config","").strip() else: nicho = data.get("nicho","tecnologia") tipo = data.get("tipo","largo") config_libre = "" token = data.get("token","") # Verificar token o sesion existente plan = "basico" if token and token in TOKENS_VALIDOS: plan = TOKENS_VALIDOS[token].get("plan", "basico") if isinstance(TOKENS_VALIDOS[token], dict) else "basico" elif email in CLIENTES and CLIENTES[email].get("plan"): plan = CLIENTES[email]["plan"] else: return JSONResponse({"error": "Token invalido"}, status_code=403) # Verificar limite del plan limite = PLANES_CANALES.get(plan, 1) canales_actuales = len(CLIENTES.get(email, {}).get("canales", [])) if canales_actuales >= limite: return JSONResponse({"error": f"Has alcanzado el limite de tu plan {plan} ({limite} canales)"}, status_code=400) # Parsear preferencias del cliente con Groq ANTES de crear config_parsed = await parsear_config_groq(config_libre) # Asignar proyecto disponible y guardarlo en config if not isinstance(config_parsed, dict): config_parsed = {} idx_disponible = proyecto_disponible() config_parsed["proyecto_idx"] = idx_disponible # Crear el canal pasando la config resultado = await crear_space(nicho, email, tipo, config_parsed, archivos_subidos) if not resultado: return JSONResponse({"error": "No se pudo crear el canal"}, status_code=500) u = resultado["url"] repo = resultado["repo"] # Guardar en CLIENTES if email not in CLIENTES: CLIENTES[email] = {"canales": [], "plan": plan} CLIENTES[email]["plan"] = plan if "canales" not in CLIENTES[email]: CLIENTES[email]["canales"] = [] CLIENTES[email]["canales"].append({"url": u, "nicho": nicho, "tipo": tipo, "repo": repo, "youtube": False, "config": config_parsed, "config_texto": config_libre}) guardar_clientes() avisar_telegram(f"🎬 Canal creado\nπŸ“§ {email}\nπŸ“Ί Plan: {plan}\n🎯 Nicho: {nicho}\nπŸ”— {u}") # Consumir token si era nuevo if token in TOKENS_VALIDOS: del TOKENS_VALIDOS[token] urls = [c["url"] for c in CLIENTES[email]["canales"]] try: enviar_email_multi(email, urls) except Exception as e: print(f"Error email: {e}", flush=True) return {"success": True, "url": u, "total": len(urls), "limite": limite} @app.post("/editar-config") async def editar_config(request: Request): data = await request.json() email = data.get("email","").strip().lower() canal_idx = data.get("canal_idx", -1) nuevo_texto = data.get("config_texto","").strip() if email not in CLIENTES: return JSONResponse({"error":"Cuenta no encontrada"}, status_code=404) canales = CLIENTES[email].get("canales", []) if canal_idx < 0 or canal_idx >= len(canales): return JSONResponse({"error":"Canal no encontrado"}, status_code=404) canal = canales[canal_idx] repo = canal.get("repo","") nicho = canal.get("nicho","tecnologia") tipo = canal.get("tipo","largo") token_hf = token_de_repo(repo) if not token_hf: return JSONResponse({"error":"No se pudo acceder al canal"}, status_code=500) # Re-parsear la nueva config con Groq config_parsed = await parsear_config_groq(nuevo_texto) if not isinstance(config_parsed, dict): config_parsed = {} # Conservar el proyecto google asignado if "proyecto_idx" in canal.get("config", {}): config_parsed["proyecto_idx"] = canal["config"]["proyecto_idx"] # Regenerar el main.py del canal con la nueva config import base64, json as _json def b64(s): return base64.b64encode(s.encode("utf-8")).decode("utf-8") with open("cliente_template.py","r") as _f: main_code = _f.read().replace("NICHO_PLACEHOLDER", nicho).replace("EMAIL_PLACEHOLDER", email).replace("REPO_PLACEHOLDER", repo).replace("GROQ_PLACEHOLDER", os.environ.get("GROQ_KEY","")).replace("GROQ_PLACEHOLDER_2", os.environ.get("GROQ_KEY_2","")).replace("GROQ_PLACEHOLDER_3", os.environ.get("GROQ_KEY_3","")).replace("PEXELS_PLACEHOLDER", os.environ.get("PEXELS_KEY","")).replace("TIPO_PLACEHOLDER", tipo).replace("CONFIG_PLACEHOLDER", _json.dumps(config_parsed)) commit_payload = { "summary": "Editar config", "files": [{"path":"main.py","content":b64(main_code),"encoding":"base64"}] } async with httpx.AsyncClient(timeout=30) as client: r = await client.post( f"https://huggingface.co/api/spaces/{repo}/commit/main", headers={"Authorization": f"Bearer {token_hf}", "Content-Type":"application/json"}, content=_json.dumps(commit_payload).encode() ) if r.status_code not in [200,201]: return JSONResponse({"error":"No se pudo aplicar"}, status_code=500) # Guardar la nueva config en CLIENTES CLIENTES[email]["canales"][canal_idx]["config"] = config_parsed CLIENTES[email]["canales"][canal_idx]["config_texto"] = nuevo_texto guardar_clientes() return {"success": True} @app.get("/mi-cuenta", response_class=HTMLResponse) async def mi_cuenta(email: str = ""): email = email.strip().lower() if not email or email not in CLIENTES: return HTMLResponse("

Cuenta no encontrada

Verifica el email correcto.

", status_code=404) cliente = CLIENTES[email] plan = cliente.get("plan", "basico") limite = PLANES_CANALES.get(plan, 1) canales = cliente.get("canales", []) restantes = limite - len(canales) lista_html = "" for i, ch in enumerate(canales, 1): ch_url = ch.get("url","#") yt_conectado = ch.get("youtube", False) if yt_conectado: yt_btn = '
YouTube conectado
' else: yt_btn = f'Conectar YouTube' cfg_texto = ch.get("config_texto","").replace('"','"').replace("\n"," ") lista_html += f'''
Canal {i}: {ch.get("nicho","-")}
{ch_url}{yt_btn}
''' if not lista_html: lista_html = '

Aun no has creado ningun canal

' boton_nuevo = "" if restantes > 0: boton_nuevo = f'+ CREAR NUEVO CANAL' TOKENS_VALIDOS[f"micuenta_{email}"] = {"plan": plan} else: boton_nuevo = '

Has alcanzado el limite de tu plan. Contacta soporte para subir de nivel.

' return HTMLResponse(f""" Mi Cuenta - TubeBot

πŸ€– Mi Cuenta TubeBot

{email}

Plan {plan}
{len(canales)}
Canales creados
{restantes}
Disponibles
{limite}
Limite plan
{boton_nuevo}

Mis canales

{lista_html}

WhatsApp soporte: +34 656 691 085

""") def enviar_email_multi(email, urls): try: import requests lista_urls = "".join([f'
  • {u}
  • ' for u in urls]) url_cuenta = f"https://cristobal299-iaweb.hf.space/mi-cuenta?email={email}" html = f"""

    TubeBot activado

    Tus {len(urls)} canales ya estan funcionando.

    Ir a Mi Cuenta

    Guarda este enlace para gestionar tus canales:
    {url_cuenta}

    Si tienes dudas responde este email.

    """ requests.post("https://api.brevo.com/v3/smtp/email", headers={"api-key": os.environ.get("BREVO_KEY",""), "Content-Type": "application/json", "accept": "application/json"}, json={ "sender": {"name": "TubeBot", "email": "dayron100813@gmail.com"}, "to": [{"email": email}], "subject": f"Tus {len(urls)} canales estan listos", "htmlContent": html } ) except Exception as e: print(f"Error email: {e}", flush=True) @app.get("/check-space") async def check_space(url: str = ""): try: async with httpx.AsyncClient(timeout=10, follow_redirects=True) as client: r = await client.get(url) if r.status_code == 200 and "gradio" in r.text.lower(): return {"ready": True} except: pass return {"ready": False} @app.get("/check-space") async def check_space(url: str = ""): try: async with httpx.AsyncClient(timeout=5) as client: r = await client.get(url) return {"ready": r.status_code == 200} except: return {"ready": False} @app.get("/esperando", response_class=HTMLResponse) async def esperando(url: str = "", email: str = ""): return f""" TubeBot β€” Preparando tu canal...

    πŸ€– CREANDO TU CANAL

    Tu sistema estΓ‘ siendo construido en nuestros servidores.

    Este proceso tarda entre 3-5 minutos la primera vez.

    ⚑ Instalando dependencias...

    ⚠️ Aviso normal de Google: VerÑs "App no verificada". Es seguro. Pulsa "Configuración avanzada" y luego "Ir a TubeBot (no seguro)". Es porque la app aún estÑ en revisión por Google.
    """ @app.get("/test-llaves") async def test_llaves(): return { "GROQ_KEY_existe": bool(os.environ.get("GROQ_KEY","")), "GROQ_KEY_longitud": len(os.environ.get("GROQ_KEY","")), "PEXELS_KEY_existe": bool(os.environ.get("PEXELS_KEY","")), "PEXELS_KEY_longitud": len(os.environ.get("PEXELS_KEY","")), } @app.get("/clientes") async def ver_clientes(): return CLIENTES @app.get("/test-token") async def test_token(): import secrets token = secrets.token_urlsafe(32) TOKENS_VALIDOS[token] = "test@test.com" return {"token": token, "url": f"/activar-canal?token={token}"} @app.get("/dashboard", response_class=HTMLResponse) async def dashboard(): total = len(CLIENTES) conectados = sum(1 for c in CLIENTES.values() if c.get("youtube_conectado")) filas = "" for email, data in CLIENTES.items(): yt = "βœ…" if data.get("youtube_conectado") else "❌" url = data.get("url","") nicho = data.get("nicho","") filas += f"{email}{nicho}{yt}Ver Space" return f"""

    πŸ€– TubeBot Dashboard

    {total}
    Clientes totales
    {conectados}
    YouTube conectado
    {total-conectados}
    Pendientes
    {filas}
    EmailNichoYouTubeSpace
    """ if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=7860) # ══════════════════════════════════════════ # YOUTUBE OAUTH # ══════════════════════════════════════════ GOOGLE_PROYECTOS = [ # (os.environ.get("GOOGLE_CLIENT_ID",""), os.environ.get("GOOGLE_CLIENT_SECRET","")), # AGOTADO # (os.environ.get("GOOGLE_CLIENT_ID_2",""), os.environ.get("GOOGLE_CLIENT_SECRET_2","")), # AGOTADO # (os.environ.get("GOOGLE_CLIENT_ID_3",""), os.environ.get("GOOGLE_CLIENT_SECRET_3","")), # AGOTADO # (os.environ.get("GOOGLE_CLIENT_ID_4",""), os.environ.get("GOOGLE_CLIENT_SECRET_4","")), # AGOTADO # (os.environ.get("GOOGLE_CLIENT_ID_5",""), os.environ.get("GOOGLE_CLIENT_SECRET_5","")), # AGOTADO (os.environ.get("GOOGLE_CLIENT_ID_6",""), os.environ.get("GOOGLE_CLIENT_SECRET_6","")), (os.environ.get("GOOGLE_CLIENT_ID_7",""), os.environ.get("GOOGLE_CLIENT_SECRET_7","")), (os.environ.get("GOOGLE_CLIENT_ID_8",""), os.environ.get("GOOGLE_CLIENT_SECRET_8","")), (os.environ.get("GOOGLE_CLIENT_ID_9",""), os.environ.get("GOOGLE_CLIENT_SECRET_9","")), ] GOOGLE_PROYECTOS = [(c,s) for c,s in GOOGLE_PROYECTOS if c and s] import datetime as _dt USOS_PROYECTO = {} # {idx: {"fecha": "2026-05-27", "count": 5}} def reset_si_nuevo_dia(idx): hoy = _dt.date.today().isoformat() if idx not in USOS_PROYECTO or USOS_PROYECTO[idx].get("fecha") != hoy: USOS_PROYECTO[idx] = {"fecha": hoy, "count": 0} def incrementar_uso(idx): reset_si_nuevo_dia(idx) USOS_PROYECTO[idx]["count"] += 1 def proyecto_disponible(): """Devuelve indice del proyecto con menos usos hoy (max 5 videos/dia)""" if not GOOGLE_PROYECTOS: return 0 contadores = [] for i in range(len(GOOGLE_PROYECTOS)): reset_si_nuevo_dia(i) contadores.append((USOS_PROYECTO[i]["count"], i)) contadores.sort() return contadores[0][1] # el indice con menos usos def get_proyecto_cliente(email): """Devuelve el proyecto Google asignado al cliente, o asigna uno nuevo""" if email in CLIENTES and "google_index" in CLIENTES[email]: idx = CLIENTES[email]["google_index"] if idx < len(GOOGLE_PROYECTOS): return GOOGLE_PROYECTOS[idx], idx # Asignar el proyecto con menos USOS hoy (mejor reparto de cuota) idx = proyecto_disponible() return GOOGLE_PROYECTOS[idx], idx # Default para arranque (se sobrescribe por cliente) if GOOGLE_PROYECTOS: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET = GOOGLE_PROYECTOS[0] else: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET = "", "" REDIRECT_URI = "https://cristobal299-iaweb.hf.space/oauth-callback" SCOPES = "https://www.googleapis.com/auth/youtube.upload" @app.get("/oauth-youtube") async def oauth_youtube(email: str, canal: int = 0): import urllib.parse (cli_id, cli_sec), idx = get_proyecto_cliente(email) if email in CLIENTES: CLIENTES[email]["google_index"] = idx guardar_clientes() params = { "client_id": cli_id, "redirect_uri": REDIRECT_URI, "response_type": "code", "scope": SCOPES, "access_type": "offline", "prompt": "consent", "state": f"{email}|{canal}" } url = "https://accounts.google.com/o/oauth2/v2/auth?" + urllib.parse.urlencode(params) from fastapi.responses import RedirectResponse return RedirectResponse(url) @app.get("/oauth-callback") async def oauth_callback(code: str, state: str): import urllib.parse if "|" in state: email, canal_idx = state.split("|", 1) canal_idx = int(canal_idx) else: email = state canal_idx = 0 (cli_id, cli_sec), idx = get_proyecto_cliente(email) async with httpx.AsyncClient() as client: r = await client.post("https://oauth2.googleapis.com/token", data={ "code": code, "client_id": cli_id, "client_secret": cli_sec, "redirect_uri": REDIRECT_URI, "grant_type": "authorization_code" }) token = r.json() if "access_token" not in token: return HTMLResponse("

    ❌ Error al obtener token

    ") # Guardar token en el Space del cliente if email in CLIENTES: canales_lista = CLIENTES[email].get("canales", []) space_repo = canales_lista[canal_idx].get("repo", "") if canal_idx < len(canales_lista) else "" import json, base64 cuentas = [ (os.environ.get("HF_TOKEN_5",""), os.environ.get("HF_USER_5","Alejandrl")), (os.environ.get("HF_TOKEN_4",""), os.environ.get("HF_USER_4","Maria288")), (os.environ.get("HF_TOKEN_2",""), os.environ.get("HF_USER_2","Cristobal2999")), (os.environ.get("HF_TOKEN_3",""), os.environ.get("HF_USER_3","Cristobal288")), ] user_repo = space_repo.split("/")[0] hf_token_repo = next((t for t,u in cuentas if u.lower()==user_repo.lower()), HF_TOKEN) token_data = dict(token) token_data["client_id"] = cli_id token_data["client_secret"] = cli_sec content_b64 = base64.b64encode(json.dumps(token_data).encode()).decode() async with httpx.AsyncClient(timeout=30) as client: r = await client.post( f"https://huggingface.co/api/spaces/{space_repo}/commit/main", headers={"Authorization": f"Bearer {hf_token_repo}", "Content-Type": "application/x-ndjson"}, content=f'{{"key":"header","value":{{"summary":"add token"}}}}\n{{"key":"file","value":{{"path":"token.json","content":"{content_b64}","encoding":"base64"}}}}\n' ) print(f"Token upload: {r.status_code} {r.text[:200]}", flush=True) # Reiniciar Space del cliente para que lea el token r2 = await client.post( f"https://huggingface.co/api/spaces/{space_repo}/restart?factory=true", headers={"Authorization": f"Bearer {hf_token_repo}"} ) print(f"Restart: {r2.status_code}", flush=True) if canal_idx < len(CLIENTES[email].get("canales", [])): CLIENTES[email]["canales"][canal_idx]["youtube"] = True guardar_clientes() url_cliente = f"https://cristobal299-iaweb.hf.space/mi-cuenta?email={email}" nicho = CLIENTES.get(email, {}).get("nicho", "tu nicho") return HTMLResponse(f""" Β‘Bienvenido!
    πŸŽ‰

    Β‘BIENVENIDO A TUBEBOT!

    Tu canal de {nicho} estΓ‘ ACTIVO βœ…

    πŸš€ Tu sistema ya estΓ‘ funcionando:

    βœ… Generando guiΓ³n con IA

    βœ… Voz profesional sintΓ©tica

    βœ… Fondo de video automΓ‘tico

    βœ… SubtΓ­tulos animados

    βœ… Publica solo en YouTube cada 4 horas

    ⏳ Reiniciando tu sistema con YouTube conectado... Espera 2 minutos.

    πŸ’‘ Guarda esta URL en favoritos:
    {url_cliente}

    ΒΏProblemas? WhatsApp: +34656691085

    """) # restart @app.post("/reportar-uso") async def reportar_uso(request: Request): """Endpoint que cada cliente Space llama cuando sube un video""" try: data = await request.json() idx = data.get("proyecto_idx", -1) email = data.get("email", "?") titulo = data.get("titulo", "?") if 0 <= idx < len(GOOGLE_PROYECTOS): incrementar_uso(idx) avisar_telegram(f"πŸ“Ί Video subido a YouTube\nπŸ“§ {email}\n🎬 {titulo[:60]}\nπŸ“Š Proyecto {idx+1}: {USOS_PROYECTO[idx]['count']} videos hoy") return {"ok": True, "usos_hoy": USOS_PROYECTO[idx]["count"]} except Exception as e: print(f"Error reportar uso: {e}", flush=True) return {"ok": False} @app.get("/estado-proyectos") async def estado_proyectos(): """Ver estado de los proyectos Google""" estado = [] for i in range(len(GOOGLE_PROYECTOS)): reset_si_nuevo_dia(i) estado.append({"proyecto": i+1, "usos_hoy": USOS_PROYECTO[i]["count"]}) return estado