feat: telegram bot + normalize for mobile + nextcloud subcarpetas
This commit is contained in:
+24
-1
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user