diff --git a/openshorts/main.py b/openshorts/main.py index 6dc47d2..456b979 100644 --- a/openshorts/main.py +++ b/openshorts/main.py @@ -991,9 +991,25 @@ if __name__ == '__main__': from concurrent.futures import ThreadPoolExecutor swears_path = "/home/ren/viral/brainrotinator/assets/swears.txt" - nextcloud_dir = "/home/ren/nextcloud/html/data/renato97/files/viral_clips" + nextcloud_dir = os.environ.get( + "NEXTCLOUD_TARGET", + "/home/ren/nextcloud/html/data/renato97/files/viral_clips" + ) os.makedirs(nextcloud_dir, exist_ok=True) + def normalize_for_mobile(path): + temp = path + ".tmp.mp4" + cmd = [ + 'ffmpeg', '-y', '-i', path, + '-c:v', 'libx264', '-preset', 'fast', '-crf', '23', + '-pix_fmt', 'yuv420p', '-profile:v', 'high', '-level:v', '4.1', + '-movflags', '+faststart', + '-c:a', 'aac', '-b:a', '128k', + temp + ] + subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) + os.replace(temp, path) + def process_clip(i, clip): start = clip['start'] end = clip['end'] @@ -1057,6 +1073,13 @@ if __name__ == '__main__': except Exception as e: print(f" ⚠️ [Clip {i+1}] Subtitle burn failed: {e}") + # Normalize for mobile + print(f" 📱 [Clip {i+1}] Normalizing for mobile playback...") + try: + normalize_for_mobile(clip_final_path) + except Exception as e: + print(f" ⚠️ [Clip {i+1}] Normalization failed: {e}") + # Copy to Nextcloud import shutil nextcloud_dest = os.path.join(nextcloud_dir, clip_filename) diff --git a/openshorts/telegram_bot.py b/openshorts/telegram_bot.py new file mode 100644 index 0000000..1865a0f --- /dev/null +++ b/openshorts/telegram_bot.py @@ -0,0 +1,185 @@ +""" +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" +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).") + + +def get_video_title(url: str) -> str: + try: + r = subprocess.run( + ["yt-dlp", "--print", "title", url], + capture_output=True, text=True, timeout=30, + ) + return r.stdout.strip() + except Exception: + 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): + title = get_video_title(url) + 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) + + 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: + proc = await asyncio.create_subprocess_exec( + sys.executable, + "-u", + str(SCRIPT_DIR / "main.py"), + "-u", + url, + "-o", + temp_dir, + 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 + + 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"\bClip\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..." + + if text and text != last_text: + last_text = text + try: + await msg.edit_text(text) + except Exception: + pass + + await proc.wait() + + 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() diff --git a/openshorts/viral-telegram.service b/openshorts/viral-telegram.service new file mode 100644 index 0000000..7532e87 --- /dev/null +++ b/openshorts/viral-telegram.service @@ -0,0 +1,17 @@ +[Unit] +Description=Viral Telegram Bot +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=ren +Group=ren +WorkingDirectory=/home/ren/viral/openshorts +ExecStart=/usr/bin/python3 /home/ren/viral/openshorts/telegram_bot.py +Restart=always +RestartSec=10 +Environment=PYTHONUNBUFFERED=1 + +[Install] +WantedBy=multi-user.target