""" Telegram bot for viral clip processing. Receives YouTube URLs, processes via main.py, sends progress updates. """ import asyncio import os import re import subprocess import sys import shutil import tempfile from datetime import datetime from pathlib import Path from dotenv import load_dotenv from telegram import Update from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes load_dotenv() BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"] CHAT_ID = int(os.environ["TELEGRAM_CHAT_ID"]) NEXTCLOUD_BASE = "/home/ren/nextcloud/html/data/renato97/files/viral_clips" VENV_PYTHON = "/home/ren/viral/venv/bin/python3" COOKIES = "/home/ren/yt/cookies.txt" SCRIPT_DIR = Path(__file__).parent queue = asyncio.Queue() current_job: str | None = None async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): if update.effective_chat.id != CHAT_ID: return await update.message.reply_text( "🎬 Mandame un link de YouTube y lo proceso.\n" "Los clips aparecen en Nextcloud automáticamente." ) async def status(update: Update, context: ContextTypes.DEFAULT_TYPE): if update.effective_chat.id != CHAT_ID: return qsize = queue.qsize() if current_job: name = current_job[:80] await update.message.reply_text(f"▶️ Procesando: {name}...\n⏳ En cola: {qsize}") elif qsize > 0: await update.message.reply_text(f"⏳ En cola: {qsize}") else: await update.message.reply_text("💤 Sin actividad.") async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE): if update.effective_chat.id != CHAT_ID: return text = update.message.text.strip() if not re.search(r"(youtube\.com|youtu\.be)", text): await update.message.reply_text("❌ Mandame un link de YouTube.") return qsize = queue.qsize() await queue.put(text) if qsize == 0 and not current_job: await update.message.reply_text("🚀 Iniciando procesamiento...") else: await update.message.reply_text(f"⏳ Encolado ({qsize + 1}º en cola).") async def get_video_title(url: str) -> str: try: r = await asyncio.to_thread( subprocess.run, ["yt-dlp", "--print", "title", url], capture_output=True, text=True, timeout=30, ) return r.stdout.strip() except subprocess.TimeoutExpired: print("[DEBUG] yt-dlp title timed out after 30s") return "video" except FileNotFoundError: print("[DEBUG] yt-dlp command not found. Trying venv yt-dlp...") try: r = await asyncio.to_thread( subprocess.run, [VENV_PYTHON, "-m", "yt_dlp", "--cookies", COOKIES, "--print", "title", url], capture_output=True, text=True, timeout=30, ) return r.stdout.strip() or "video" except Exception as e2: print(f"[DEBUG] venv yt-dlp also failed: {e2}") return "video" except Exception as e: print(f"[DEBUG] yt-dlp title failed: {e}") return "video" async def process_queue(bot): global current_job while True: url = await queue.get() current_job = url try: await process_video(bot, url) except Exception as e: try: await bot.send_message(CHAT_ID, f"❌ Error procesando: {e}") except Exception: pass finally: current_job = None queue.task_done() async def process_video(bot, url: str): print(f"[DEBUG] process_video: url={url[:80]}") title = await get_video_title(url) print(f"[DEBUG] title={title}") safe = re.sub(r'[<>:"/\\|?*#\s]+', "_", title)[:80] folder = f"{safe}_{datetime.now():%Y-%m-%d}" target = f"{NEXTCLOUD_BASE}/{folder}" os.makedirs(target, exist_ok=True) print(f"[DEBUG] NEXTCLOUD_TARGET={target}") msg = await bot.send_message(CHAT_ID, "🚀 Descargando...") env = os.environ.copy() env["NEXTCLOUD_TARGET"] = target temp_dir = tempfile.mkdtemp(prefix="viral_") total_clips = 0 cur_clip = 0 last_text = "" try: cmd = [ str(SCRIPT_DIR / "main.py"), "-u", url, "-o", temp_dir, ] print(f"[DEBUG] Spawning: {' '.join(cmd)}") proc = await asyncio.create_subprocess_exec( VENV_PYTHON, "-u", *cmd, env=env, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, ) async for raw in proc.stdout: line = raw.decode("utf-8", errors="replace").strip() if not line: continue print(f"[DEBUG] stdout: {line[:100]}") text = None if "Transcribing" in line: text = "🎙️ Transcribiendo..." elif "Calling local LLM" in line: text = "🤖 Analizando clips virales..." elif m := re.search(r"Found\s+(\d+)\s+viral", line, re.IGNORECASE): total_clips = int(m.group(1)) text = f"🔥 Encontrados {total_clips} clips" elif m := re.search(r"\[Clip\s+(\d+)\]\s+Starting", line): cur_clip = int(m.group(1)) text = f"🎬 Clip {cur_clip}/?: reencuadrando + subs..." elif "Normalizing" in line and cur_clip > 0: text = f"📱 Clip {cur_clip}/{max(total_clips, cur_clip)}: normalizando..." elif "Copying to Nextcloud" in line and cur_clip > 0: text = f"📤 Clip {cur_clip}/{max(total_clips, cur_clip)}: subiendo a Nextcloud..." elif "Total execution time" in line: text = None if text and text != last_text: last_text = text try: await msg.edit_text(text) except Exception: pass await proc.wait() print(f"[DEBUG] main.py exit code: {proc.returncode}") finally: shutil.rmtree(temp_dir, ignore_errors=True) try: await msg.delete() except Exception: pass async def post_init(app: Application): asyncio.create_task(process_queue(app.bot)) def main(): app = ( Application.builder() .token(BOT_TOKEN) .post_init(post_init) .build() ) app.add_handler(CommandHandler("start", start)) app.add_handler(CommandHandler("status", status)) app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message)) print("🤖 Viral Telegram Bot iniciado.") app.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main()