Files
viral/openshorts/telegram_bot.py
T

216 lines
6.6 KiB
Python

"""
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()