Repository: shamilmyran/sub-telegram
Branch: main
Commit: 5ee184efdb71
Files: 4
Total size: 104.6 KB
Directory structure:
gitextract_ds0fbhii/
├── .gitignore
├── README.md
├── bot.py
└── requirements.txt
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Python
__pycache__/
*.py[cod]
*.so
*.egg-info/
dist/
build/
# Virtual environment
venv/
env/
.env
# Bot data & logs
bot_data.json
bot.log
*.log
# IDE
.vscode/
.idea/
# OS junk
.DS_Store
Thumbs.db
================================================
FILE: README.md
================================================
# MunaX Subtitle Engine – True Final Boss
A lightning‑fast Telegram bot that searches 5 subtitle sources, delivers Malayalam subtitles instantly, and translates any language to Malayalam.
## Features
- 🔍 Concurrent search over 5 sources
- 🇮🇳 Malayalam‑first results
- 🌐 Translate any subtitle to Malayalam
- 📢 Broadcast to all users + groups
- 🏆 Leaderboard & trending searches
- 📥 Subtitle request system
- 🔧 Maintenance mode
- 🎨 Premium Malayalam UI
## Deploy
1. Clone this repository.
2. Install dependencies: `pip install -r requirements.txt`
3. Set environment variables: `BOT_TOKEN`, `RENDER_URL` (optional).
4. Run `python bot.py`.
## Admin Commands
- `/stats` – Bot statistics
- `/broadcast` – Send message to all users + groups
- `/maintenance on/off` – Toggle maintenance mode
- `/requests` – View pending subtitle requests
- `/donereq <title>` – Mark request as done
- `/userinfo <id>` – Detailed user info
- `/clearcache` – Clear search cache
## Credits
Built with ❤️ by @munax9 for Malayalam cinema lovers.
================================================
FILE: bot.py
================================================
#!/usr/bin/env python3
"""
╔══════════════════════════════════════════════════════════════╗
║ 𝗠𝘂𝗻𝗮𝗫 𝗦𝘂𝗯𝘀 ☠️⚡ ULTRA LEGENDARY FINAL ║
║ Premium Malayalam Subtitle Bot for Telegram ║
║ Version 3.0 — The Last Boss. No Bugs. No Mercy. ║
╚══════════════════════════════════════════════════════════════╝
COMPLETE FIX LOG vs previous versions:
[F01] No placeholder lambdas — all 28 commands fully implemented
[F02] Module-level asyncio primitives removed — all inside BotHandlers
[F03] RateLimiter uses lazy lock (no event-loop-before-init crash)
[F04] Database maintenance row inserted on init
[F05] cmd_help uses HTML parse_mode (not Markdown)
[F06] Translation respects user's default_lang pref
[F07] is_valid_srt: ≥200 chars + ≥5 blocks
[F08] Malayalam fast-path retries up to 3 sources
[F09] Progress callback throttled (≥1.2s between edits)
[F10] gate_user shows correct join-required message
[F11] Deduplication of results by download URL
[F12] read_file_content min-size guard (>100 chars)
[F13] Event loop conflict fixed — db.init() in post_init only
[F14] vtt_to_srt dot escaped (literal dot not wildcard) in timestamp regex
[F15] OMDB uses HTTPS
[F16] _scrape_get has exponential backoff
[F17] search_goat uses _scrape_get (retry + session)
[F18] Bounded cache with LRU eviction (500 entries)
[F19] WAL + synchronous=NORMAL before CREATE TABLE
[F20] Graceful DB close in post_stop
[F21] Relative URL resolution with urljoin
[F22] Broadcast flood-safe (asyncio.sleep 0.05 between sends)
[F23] Leaderboard rank by user_id not download count
[F24] search_all uses instance semaphore, not module-level
[F25] _deep_block chunks text >4800 chars (no truncation)
[F26] Auto-retry fallback for all 3 Malayalam sources
[F27] Inline query support (@botname Movie)
[F28] Smart result ranking with title similarity scoring
"""
from __future__ import annotations
import os
import re
import time
import asyncio
import json
import tempfile
import zipfile
import contextlib
import random
import threading
import html
import logging
import uuid
from datetime import datetime
from typing import Optional, List, Tuple, Dict, Any, Callable
from logging.handlers import RotatingFileHandler
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import quote_plus, urljoin
import aiosqlite
import requests
import httpx
from bs4 import BeautifulSoup
from deep_translator import GoogleTranslator
from telegram import (
Update, InlineKeyboardButton, InlineKeyboardMarkup,
InlineQueryResultArticle, InputTextMessageContent,
Message, constants,
)
from telegram.ext import (
Application, CommandHandler, MessageHandler, filters,
CallbackQueryHandler, InlineQueryHandler, ContextTypes,
)
from telegram.error import TelegramError
# ═══════════════════════════════════════════════════════════════
# CONFIGURATION
# ═══════════════════════════════════════════════════════════════
BOT_TOKEN = os.environ.get("BOT_TOKEN")
if not BOT_TOKEN:
raise RuntimeError("BOT_TOKEN environment variable is missing")
ADMIN_IDS = [
int(x) for x in os.environ.get("ADMIN_IDS", "8361710122,1591775154").split(",")
]
UPI_ID = "munavirmunu1010@okicici"
GROUP_LINK = "https://t.me/malayalamsubhub"
GROUP_USERNAME = "malayalamsubhub"
GROUP_ID = os.environ.get("GROUP_ID", f"@{GROUP_USERNAME}")
RENDER_URL = os.environ.get("RENDER_URL", "")
PROXY_LIST = [p.strip() for p in os.environ.get("PROXY_LIST", "").split(",") if p.strip()]
WYZIE_API_KEY = os.environ.get("WYZIE_API_KEY", "wyzie-33d7dd9a214b4424579c611653a189c1")
OMDB_API_KEY = os.environ.get("OMDB_API_KEY", "743db943")
JERRY_SEARCH = "https://jerrycoder.oggyapi.workers.dev/search/subtitle"
JERRY_DOWNLOAD = "https://jerrycoder.oggyapi.workers.dev/download/subtitle"
IRON_SEARCH = "https://ironman.koyeb.app/search/subtitles"
IRON_DOWNLOAD = "https://ironman.koyeb.app/download/subtitles"
PAGE_SIZE = 8
RATE_LIMIT_S = 4
MAX_FILE_SIZE = 5 * 1024 * 1024
CACHE_TTL = 3600
MAX_CACHE_ENTRIES = 500
MEMBERSHIP_CACHE_TTL = 300
MAX_HISTORY = 15
MAX_REQUESTS_PER_USER = 10
TRANSLATE_TIMEOUT = 180
MAX_CONCURRENT_TRANSLATIONS = 3
MAX_CONCURRENT_SEARCH = 5
VERSION = "Ultra Legendary Final ☠️⚡"
BRAND = "𝗠𝘂𝗻𝗮𝗫 𝗦𝘂𝗯𝘀"
LANG_MAP = {
"en": "English", "ml": "Malayalam", "hi": "Hindi",
"ta": "Tamil", "te": "Telugu", "kn": "Kannada",
"bn": "Bengali", "mr": "Marathi", "gu": "Gujarati",
"pa": "Punjabi", "ur": "Urdu", "fr": "French",
"es": "Spanish", "de": "German", "it": "Italian",
"pt": "Portuguese", "ru": "Russian", "zh": "Chinese",
"ja": "Japanese", "ko": "Korean", "ar": "Arabic",
"id": "Indonesian", "ms": "Malay", "th": "Thai",
"vi": "Vietnamese", "tr": "Turkish", "nl": "Dutch",
"sv": "Swedish", "pl": "Polish", "el": "Greek",
}
CHAT_WORDS = frozenset({
"ok","okay","lol","haha","nice","good","bad","wow","bro","da","ente","njan",
"nee","ningal","yes","no","thanks","thank","please","what","why","how","when",
"where","who","which","are","is","was","the","and","but","for","not","with",
"this","that","they","have","been","will","can","may","should","would","could",
"just","very","also","from","into","about","than","then","more","some","any",
"all","both","each","few","most","other","such","only","same","over","here",
"there","hi","hello","hey","bye","wait","actually","really","already","still",
"again","ever","never","always","sometimes","maybe","probably","send","get",
"give","make","kitto","see","think","come","go","put","use","look","want",
"need","mm","said","say","tell","venam","show","back","after","before","now",
"so","as","at","by","do","did","done","its","it","him","her","his","our",
"their","myr","my","we","us","he","she","one","two","three","four","five",
"aanu","alle","anu","athu","ayyo","chetta","chechi","enthanu","enthina","evidea",
"ipo","ithu","ivide","ketto","mone","mole","poda","podi","sheriyanu","ano",
"undo","und","illa","ille","dei","di","machane","machi",
})
DEFAULT_BANNED = ("jav","porn","xxx","hentai","sexy","adult")
_banned_env = os.environ.get("BANNED_TERMS", "")
BANNED_TERMS = tuple(_banned_env.lower().split(",")) if _banned_env else DEFAULT_BANNED
SOURCE_LABEL = {
"wyzie": "Wyzie",
"goat": "Team GOAT",
"malsub": "MSone",
"jerry": "SubScene",
"ironman": "OpenSub",
}
# ═══════════════════════════════════════════════════════════════
# PREMIUM UI
# ═══════════════════════════════════════════════════════════════
def pb(text: str) -> str:
"""Bold mathematical sans-serif font."""
m = str.maketrans(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
"𝗔𝗕𝗖𝗗𝗘𝗙𝗚𝗛𝗜𝗝𝗞𝗟𝗠𝗡𝗢𝗣𝗤𝗥𝗦𝗧𝗨𝗩𝗪𝗫𝗬𝗭𝗮𝗯𝗰𝗱𝗲𝗳𝗴𝗵𝗶𝗷𝗸𝗹𝗺𝗻𝗼𝗽𝗾𝗿𝘀𝘁𝘂𝘃𝘄𝘅𝘆𝘇𝟬𝟭𝟮𝟯𝟰𝟱𝟲𝟳𝟴𝟵"
)
return text.translate(m)
def bar(frac: float, w: int = 18) -> str:
f = max(0, min(w, int(w * frac)))
return f"[{'█' * f}{'░' * (w - f)}] {int(frac * 100)}%"
def ui_box(icon: str, title: str, body: str) -> str:
return (
f"╔══════════════════════════════════════╗\n"
f"║ {icon} {pb(title):<33}║\n"
f"╚══════════════════════════════════════╝\n\n"
f"{body}"
)
def ui_start(name: str, downloads: int = 0) -> str:
dl_line = f"\n📥 Downloads: <b>{downloads}</b>" if downloads else ""
return ui_box("☠️", "MUNAX SUBS",
f"✨ സ്വാഗതം, <b>{escape(name)}</b>! ✨{dl_line}\n\n"
f"┌────────────────────────────────────┐\n"
f"│ 🎬 Cinema / Series title type ചെയ്യൂ │\n"
f"│ ⚡ 5 sources · Malayalam first │\n"
f"└────────────────────────────────────┘\n\n"
f"📌 <i>Example:</i>\n"
f" • <code>Kantara</code>\n"
f" • <code>Dune 2021</code>\n"
f" • <code>Breaking Bad S01E05</code>\n\n"
f"──────────────────────────────────────\n"
f"/help · /history · /trending · /request"
)
def ui_join_required(name: str) -> str:
return ui_box("🔒", "Join Required",
f"<b>{escape(name)}</b>, bot use ചെയ്യാൻ ആദ്യം\n"
f"<b>Malayalam Subtitle Hub</b> join ചെയ്യണം.\n\n"
f"Join ചെയ്ത് ✅ Verify press ചെയ്യൂ."
)
def ui_searching(q: str, lines: List[str]) -> str:
progress = "".join(lines)
return (
f"╔══════════════════════════════════════╗\n"
f"║ 🔍 {pb('Searching'):<33}║\n"
f"╚══════════════════════════════════════╝\n\n"
f"🎬 <b>{escape(q[:40])}</b>\n\n"
f"{progress}"
)
def ui_results(q: str, total: int) -> str:
return ui_box("🎬", "Results",
f"🎬 <b>{escape(q[:40])}</b>\n\n"
f"📦 <b>{total} results</b> — ഏത് വേണം?\n"
f"🇮🇳 = Malayalam direct | 🎬 = Other lang"
)
def ui_not_found(q: str) -> str:
return ui_box("❌", "Not Found",
f"🎬 <i>{escape(q)}</i>\n\n"
f"Subtitle ലഭ്യമല്ല.\n\n"
f"💡 Try:\n"
f" • English title use ചെയ്യൂ\n"
f" • Year add ചെയ്യൂ: <code>Dune 2021</code>\n"
f" • Series: <code>BB S01E05</code>\n\n"
f"📥 /request {escape(q)}"
)
def ui_downloading() -> str:
return ui_box("📡", "Downloading",
f"{bar(0.4)} ⚡ Subtitle file fetch ചെയ്യുന്നു…"
)
def ui_sent(title: str) -> str:
return ui_box("✅", "Ready",
f"🎬 <b>{escape(title[:50])}</b>\n\n"
f"⚡ Subtitle ചാറ്റിൽ ready!\n\n"
f"──────────────────────────────────────\n"
f"🌐 Malayalam-ലേക്ക് translate ചെയ്യണോ?"
)
def ui_translate_start() -> str:
return ui_box("🌐", "Translating",
f"{bar(0.1)} ⚡ Translation engine start ചെയ്യുന്നു…\n\n"
f"<i>Large files → ~2 min. Please wait.</i>"
)
def ui_translate_progress(done: int, total: int) -> str:
frac = done / total if total else 0
return (
f"╔══════════════════════════════════════╗\n"
f"║ 🌐 {pb('Translating'):<33}║\n"
f"╚══════════════════════════════════════╝\n\n"
f"{bar(frac)} ⚡ {done}/{total} batches\n\n"
f"<i>Please wait…</i>"
)
def ui_translate_done() -> str:
return ui_box("🇮🇳", "Done",
f"✨ <b>Malayalam subtitle ready!</b> ✨\n\n"
f"🎬 Enjoy the movie! 🍿\n\n"
f"──────────────────────────────────────\n"
f"{pb('MUNAX SUBS')} ☠️"
)
def ui_translate_fail() -> str:
return "☠️ <b>Translation failed.</b>\n\nFile too large or service down.\n/translate ചെയ്ത് retry ചെയ്യൂ."
def ui_large_file(size_kb: float) -> str:
return (
f"⚠️ <b>Large file</b> ({size_kb:.0f} KB)\n\n"
f"Translation ~3 min ആകും. Continue?"
)
# ═══════════════════════════════════════════════════════════════
# KEYBOARDS
# ═══════════════════════════════════════════════════════════════
def kb_join() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup([
[InlineKeyboardButton("👥 Malayalam Subtitle Hub Join ചെയ്യൂ", url=GROUP_LINK)],
[InlineKeyboardButton("✅ Join ചെയ്തു — Verify", callback_data="verify")],
])
def kb_translate() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup([
[InlineKeyboardButton("🇮🇳 Malayalam-ലേക്ക് Translate", callback_data="translate")],
[InlineKeyboardButton("✕ വേണ്ട", callback_data="skip")],
])
def kb_back(page: int) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup([[
InlineKeyboardButton("⟨ Results-ലേക്ക് Back", callback_data=f"pg_{page}"),
]])
def kb_large_file() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup([
[InlineKeyboardButton("✅ Continue", callback_data="translate_force")],
[InlineKeyboardButton("✕ Cancel", callback_data="skip")],
])
def kb_results(results: List[dict], page: int, query: str) -> Tuple[InlineKeyboardMarkup, int, int]:
start = page * PAGE_SIZE
chunk = results[start:start + PAGE_SIZE]
total = len(results)
pages = max(1, (total + PAGE_SIZE - 1) // PAGE_SIZE)
rows = []
for i, r in enumerate(chunk):
idx = start + i
title = r.get("title", query)[:52]
badge = "🇮🇳" if "malayalam" in r.get("language", "").lower() else "🎬"
rows.append([InlineKeyboardButton(f"{badge} {title}", callback_data=f"sub_{idx}")])
nav = []
if page > 0:
nav.append(InlineKeyboardButton("⟨ Prev", callback_data=f"pg_{page - 1}"))
nav.append(InlineKeyboardButton(f"{page + 1}/{pages}", callback_data="noop"))
if page < pages - 1:
nav.append(InlineKeyboardButton("Next ⟩", callback_data=f"pg_{page + 1}"))
if nav:
rows.append(nav)
rows.append([InlineKeyboardButton("⟳ New Search", callback_data="new_search")])
return InlineKeyboardMarkup(rows), len(chunk), total
def kb_langs(langs: List[dict], page: int) -> Tuple[InlineKeyboardMarkup, List[dict]]:
sl = sorted(langs, key=lambda x: (0 if x["language"].lower() == "english" else 1, x["language"].lower()))
rows = [[InlineKeyboardButton(f"🌐 {l['language']}", callback_data=f"lang_{i}")] for i, l in enumerate(sl)]
rows.append([InlineKeyboardButton("⟨ Back", callback_data=f"pg_{page}")])
return InlineKeyboardMarkup(rows), sl
# ═══════════════════════════════════════════════════════════════
# LOGGING
# ═══════════════════════════════════════════════════════════════
logging.basicConfig(
format="%(asctime)s [%(levelname)-8s] %(message)s",
datefmt="%H:%M:%S",
level=logging.INFO,
handlers=[
logging.StreamHandler(),
RotatingFileHandler("bot.log", maxBytes=5 * 1024 * 1024, backupCount=3),
],
)
logger = logging.getLogger(__name__)
# ═══════════════════════════════════════════════════════════════
# DATABASE
# ═══════════════════════════════════════════════════════════════
class Database:
def __init__(self, path: str = "bot_data.db"):
self.path = path
self.conn: Optional[aiosqlite.Connection] = None
async def init(self):
self.conn = await aiosqlite.connect(self.path)
# [F19] WAL before CREATE TABLE
await self.conn.execute("PRAGMA journal_mode=WAL")
await self.conn.execute("PRAGMA synchronous=NORMAL")
await self.conn.executescript("""
CREATE TABLE IF NOT EXISTS users (
user_id INTEGER PRIMARY KEY,
username TEXT,
first_name TEXT,
first_seen REAL,
downloads INTEGER DEFAULT 0,
banned BOOLEAN DEFAULT 0
);
CREATE TABLE IF NOT EXISTS history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
query TEXT,
source TEXT,
timestamp REAL,
FOREIGN KEY(user_id) REFERENCES users(user_id)
);
CREATE TABLE IF NOT EXISTS groups (group_id INTEGER PRIMARY KEY);
CREATE TABLE IF NOT EXISTS requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
username TEXT,
title TEXT,
timestamp REAL,
done BOOLEAN DEFAULT 0
);
CREATE TABLE IF NOT EXISTS trending (
query TEXT PRIMARY KEY,
count INTEGER DEFAULT 1,
last_updated REAL
);
CREATE TABLE IF NOT EXISTS user_prefs (
user_id INTEGER PRIMARY KEY,
auto_translate BOOLEAN DEFAULT 0,
default_lang TEXT DEFAULT 'ml'
);
CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT);
CREATE INDEX IF NOT EXISTS idx_history_user ON history(user_id);
CREATE INDEX IF NOT EXISTS idx_requests_done ON requests(done);
CREATE INDEX IF NOT EXISTS idx_trending_last ON trending(last_updated);
""")
# [F04] maintenance row
await self.conn.execute(
"INSERT OR IGNORE INTO config(key,value) VALUES('maintenance','0')"
)
await self.conn.commit()
async def close(self):
if self.conn:
await self.conn.close()
self.conn = None
async def update_user(self, user_id: int, username: str, first_name: str):
await self.conn.execute(
"INSERT INTO users(user_id,username,first_name,first_seen,downloads) VALUES(?,?,?,?,0) "
"ON CONFLICT(user_id) DO UPDATE SET username=excluded.username,first_name=excluded.first_name",
(user_id, username or "", first_name or "", time.time()),
)
await self.conn.commit()
async def add_history(self, user_id: int, query: str, source: str = ""):
await self.conn.execute(
"INSERT INTO history(user_id,query,source,timestamp) VALUES(?,?,?,?)",
(user_id, query, source, time.time()),
)
await self.conn.commit()
await self.conn.execute(
"DELETE FROM history WHERE id NOT IN "
"(SELECT id FROM history WHERE user_id=? ORDER BY timestamp DESC LIMIT ?)",
(user_id, MAX_HISTORY),
)
await self.conn.commit()
async def get_history(self, user_id: int) -> List[Dict]:
cur = await self.conn.execute(
"SELECT query,source,timestamp FROM history WHERE user_id=? ORDER BY timestamp DESC LIMIT ?",
(user_id, MAX_HISTORY),
)
return [{"q": r[0], "src": r[1], "t": r[2]} for r in await cur.fetchall()]
async def increment_downloads(self, user_id: int):
await self.conn.execute(
"UPDATE users SET downloads=downloads+1 WHERE user_id=?", (user_id,)
)
await self.conn.commit()
async def get_user_info(self, user_id: int) -> Optional[Dict]:
cur = await self.conn.execute(
"SELECT username,first_name,first_seen,downloads FROM users WHERE user_id=?",
(user_id,),
)
row = await cur.fetchone()
return (
{"username": row[0], "first_name": row[1], "first_seen": row[2], "downloads": row[3]}
if row else None
)
async def is_banned(self, user_id: int) -> bool:
cur = await self.conn.execute("SELECT banned FROM users WHERE user_id=?", (user_id,))
row = await cur.fetchone()
return bool(row and row[0])
async def ban_user(self, user_id: int):
await self.conn.execute("UPDATE users SET banned=1 WHERE user_id=?", (user_id,))
await self.conn.commit()
async def unban_user(self, user_id: int):
await self.conn.execute("UPDATE users SET banned=0 WHERE user_id=?", (user_id,))
await self.conn.commit()
async def get_banned_users(self) -> List[int]:
cur = await self.conn.execute("SELECT user_id FROM users WHERE banned=1")
return [r[0] for r in await cur.fetchall()]
async def add_group(self, group_id: int):
await self.conn.execute(
"INSERT OR IGNORE INTO groups(group_id) VALUES(?)", (group_id,)
)
await self.conn.commit()
async def get_all_groups(self) -> List[int]:
cur = await self.conn.execute("SELECT group_id FROM groups")
return [r[0] for r in await cur.fetchall()]
async def track_trending(self, query: str):
now = time.time()
await self.conn.execute(
"INSERT INTO trending(query,count,last_updated) VALUES(?,1,?) "
"ON CONFLICT(query) DO UPDATE SET count=count+1,last_updated=?",
(query, now, now),
)
await self.conn.commit()
await self.conn.execute(
"DELETE FROM trending WHERE last_updated<?", (now - 30 * 86400,)
)
await self.conn.commit()
async def get_trending(self, limit: int = 10) -> List[Tuple[str, int]]:
cur = await self.conn.execute(
"SELECT query,count FROM trending ORDER BY count DESC LIMIT ?", (limit,)
)
return [(r[0], r[1]) for r in await cur.fetchall()]
async def add_request(self, user_id: int, username: str, title: str) -> bool:
cur = await self.conn.execute(
"SELECT COUNT(*) FROM requests WHERE user_id=? AND done=0", (user_id,)
)
if (await cur.fetchone())[0] >= MAX_REQUESTS_PER_USER:
return False
await self.conn.execute(
"INSERT INTO requests(user_id,username,title,timestamp) VALUES(?,?,?,?)",
(user_id, username, title, time.time()),
)
await self.conn.commit()
return True
async def get_pending_requests(self, limit: int = 20) -> List[Dict]:
cur = await self.conn.execute(
"SELECT id,user_id,username,title,timestamp FROM requests "
"WHERE done=0 ORDER BY timestamp DESC LIMIT ?",
(limit,),
)
return [
{"id": r[0], "uid": r[1], "username": r[2], "title": r[3], "t": r[4]}
for r in await cur.fetchall()
]
async def mark_request_done(self, title: str) -> int:
cur = await self.conn.execute(
"UPDATE requests SET done=1 WHERE title LIKE ? AND done=0", (f"%{title}%",)
)
await self.conn.commit()
return cur.rowcount
async def get_leaderboard(self, limit: int = 10) -> List[Tuple[int, str, int]]:
# [F23] returns user_id for accurate rank comparison
cur = await self.conn.execute(
"SELECT user_id,COALESCE(first_name,username,'User'),downloads "
"FROM users WHERE downloads>0 ORDER BY downloads DESC LIMIT ?",
(limit,),
)
return [(r[0], r[1], r[2]) for r in await cur.fetchall()]
async def get_user_pref(self, user_id: int, key: str, default: Any = None) -> Any:
cur = await self.conn.execute(
"SELECT auto_translate,default_lang FROM user_prefs WHERE user_id=?", (user_id,)
)
row = await cur.fetchone()
if not row:
return default
if key == "auto_translate":
return bool(row[0])
if key == "default_lang":
return row[1]
return default
async def set_user_pref(self, user_id: int, key: str, value: Any):
if key == "auto_translate":
await self.conn.execute(
"INSERT INTO user_prefs(user_id,auto_translate) VALUES(?,?) "
"ON CONFLICT(user_id) DO UPDATE SET auto_translate=?",
(user_id, int(value), int(value)),
)
elif key == "default_lang":
await self.conn.execute(
"INSERT INTO user_prefs(user_id,default_lang) VALUES(?,?) "
"ON CONFLICT(user_id) DO UPDATE SET default_lang=?",
(user_id, value, value),
)
await self.conn.commit()
async def get_total_stats(self) -> Tuple[int, int]:
cur = await self.conn.execute("SELECT COUNT(*),SUM(downloads) FROM users")
row = await cur.fetchone()
return (row[0] or 0, row[1] or 0)
async def get_all_user_ids(self) -> List[int]:
cur = await self.conn.execute("SELECT user_id FROM users")
return [r[0] for r in await cur.fetchall()]
async def is_maintenance(self) -> bool:
cur = await self.conn.execute("SELECT value FROM config WHERE key='maintenance'")
row = await cur.fetchone()
return bool(row and row[0] == "1")
async def set_maintenance(self, state: bool):
await self.conn.execute(
"UPDATE config SET value=? WHERE key='maintenance'", ("1" if state else "0",)
)
await self.conn.commit()
# ═══════════════════════════════════════════════════════════════
# UTILITIES
# ═══════════════════════════════════════════════════════════════
def escape(s: str) -> str:
return html.escape(s or "")
def is_valid_srt(content: str) -> bool:
"""[F07] ≥200 chars AND ≥5 subtitle blocks."""
if not content or len(content) < 200:
return False
blocks = [b for b in re.split(r"\n{2,}", content.strip()) if b.strip()]
if len(blocks) < 5:
return False
return bool(
re.search(
r"\d{1,2}:\d{2}:\d{2}[,.]\d{3}\s*-->\s*\d{1,2}:\d{2}:\d{2}[,.]\d{3}",
content,
)
)
def vtt_to_srt(content: str) -> str:
"""[F14] Dots escaped (literal dot, not wildcard) in timestamp regex."""
content = re.sub(r"^WEBVTT[^\n]*\n", "", content)
content = re.sub(r"NOTE\b[^\n]*(?:\n[^\n]+)*", "", content)
content = re.sub(r"STYLE\b[^\n]*(?:\n[^\n]+)*", "", content)
content = re.sub(r"(\d{2}:\d{2}:\d{2})\.(\d{3})", r"\1,\2", content) # FIX: \.
content = re.sub(r"\b(\d{2}:\d{2})\.(\d{3})\b", r"00:\1,\2", content) # FIX: \.
content = re.sub(r"(<\d{2}:\d{2}:\d{2}\.\d{3}>)", "", content)
content = re.sub(r"\s+(align|position|line|size|region):[^\s]+", "", content)
content = re.sub(r"<[^>]+>", "", content)
blocks = [b.strip() for b in re.split(r"\n{2,}", content.strip()) if b.strip()]
out, seq = [], 1
for block in blocks:
lines = block.split("\n")
has_tc = any(re.match(r"\d{2}:\d{2}:\d{2},\d{3}", ln) for ln in lines)
if not has_tc:
continue
if lines and not re.match(r"\d{2}:\d{2}:\d{2},\d{3}", lines[0]) and not re.match(r"^\d+$", lines[0]):
lines = lines[1:]
if not lines:
continue
if re.match(r"^\d+$", lines[0]):
lines[0] = str(seq)
else:
lines.insert(0, str(seq))
out.append("\n".join(lines))
seq += 1
return "\n\n".join(out)
def clean_filename(text: str) -> str:
return re.sub(r"\s+", "_", re.sub(r"[^\w\s-]", "", text).strip())[:100]
def make_filename(movie: str, ext: str = ".srt") -> str:
return f"{clean_filename(movie)}{ext}"
def clean_language_name(lang: str) -> str:
lang = re.sub(r"[^\w\s-]", "", lang).strip()
return lang.title() or "Unknown"
def is_movie_query(text: str) -> bool:
t = text.strip()
if len(t) < 2:
return False
if re.search(r"[\u0D00-\u0D7F]", t):
return True
words = t.split()
if len(words) > 9 or t.endswith("?"):
return False
if words and words[0].lower() in {
"what","why","how","when","where","who","is","are",
"can","will","does","did","has","have",
}:
return False
alpha = {w.lower() for w in words if w.isalpha()}
if alpha and alpha.issubset(CHAT_WORDS):
return False
if re.search(r"\b(19|20)\d{2}\b", t):
return True
if re.search(r"\bS\d{1,2}E\d{1,2}\b", t, re.I):
return True
if re.search(r"\bseason\s+\d+\b", t, re.I):
return True
if any(w[0].isupper() for w in words if w.isalpha()):
return True
non_chat = [w for w in alpha if len(w) >= 3 and w not in CHAT_WORDS]
return bool(non_chat)
def is_clean_query(t: str) -> bool:
return not any(b in t.lower() for b in BANNED_TERMS)
def _score_title(result_title: str, query: str) -> int:
"""[F28] Proper title similarity score."""
_NOISE = {"the","a","an","of","and","or","in","on","at","to","for","with","part"}
def _norm(s: str) -> str:
s = re.sub(r"[^a-z0-9\s]", " ", s.lower())
return re.sub(r"\s+", " ", s).strip()
rt, qt = _norm(result_title), _norm(query)
rt_w, qt_w = [w for w in rt.split() if w not in _NOISE], [w for w in qt.split() if w not in _NOISE]
if not qt_w:
return 0
if rt == qt:
return 100
matched = sum(1 for w in qt_w if w in rt_w)
if matched == len(qt_w):
return 80 + matched
year_m = re.search(r"\b(19|20)\d{2}\b", query)
if year_m and year_m.group() in result_title:
return 60 + matched
if matched >= max(1, len(qt_w) // 2):
return 40 + matched
if qt.replace(" ", "") in rt.replace(" ", ""):
return 30
return 0
def rank_results(results: List[dict], query: str) -> List[dict]:
for item in results:
lang = item.get("language", "").lower()
if "malayalam" in lang or lang == "ml":
item["_score"] = 1000
else:
item["_score"] = _score_title(item.get("title", ""), query)
return sorted(results, key=lambda x: x["_score"], reverse=True)
def deduplicate(results: List[dict]) -> List[dict]:
"""[F11] Remove entries with identical download URLs."""
seen: set = set()
out: List[dict] = []
for r in results:
url = r.get("download", r.get("url", ""))
if url and url not in seen:
seen.add(url)
out.append(r)
elif not url:
out.append(r)
return out
# ═══════════════════════════════════════════════════════════════
# HTTP
# ═══════════════════════════════════════════════════════════════
def http_get(
url: str,
params: dict = None,
timeout: int = 60,
retries: int = 3,
headers: dict = None,
) -> Optional[requests.Response]:
if headers is None:
headers = {"User-Agent": "Mozilla/5.0"}
proxy_pool = PROXY_LIST[:] if PROXY_LIST else [None]
for attempt in range(retries):
proxy = random.choice(proxy_pool) if proxy_pool else None
proxies = {"http": proxy, "https": proxy} if proxy else None
try:
r = requests.get(url, params=params, timeout=timeout, headers=headers, proxies=proxies)
r.raise_for_status()
return r
except Exception:
if attempt == retries - 1:
return None
time.sleep(2 ** attempt)
return None
_SCRAPE_SESSION = requests.Session()
_SCRAPE_SESSION.headers.update({"User-Agent": "Mozilla/5.0", "Accept-Language": "en-US,en;q=0.9"})
def _scrape_get(url: str, params: dict = None, timeout: int = 15) -> Optional[requests.Response]:
"""[F16] Exponential backoff between retries."""
for attempt in range(3):
try:
r = _SCRAPE_SESSION.get(url, params=params, timeout=timeout, allow_redirects=True)
r.raise_for_status()
if len(r.content) < 100:
return None
return r
except Exception:
if attempt == 2:
return None
time.sleep(2 ** attempt)
return None
# ═══════════════════════════════════════════════════════════════
# FILE DOWNLOAD
# ═══════════════════════════════════════════════════════════════
def extract_zip_srt(zip_path: str) -> Optional[str]:
try:
with zipfile.ZipFile(zip_path, "r") as z:
srts = [n for n in z.namelist() if n.lower().endswith(".srt")]
if not srts:
return None
target = next((n for n in srts if "mal" in n.lower()), srts[0])
data = z.read(target)
if len(data) > MAX_FILE_SIZE:
return None
tmp = tempfile.NamedTemporaryFile(mode="wb", suffix=".srt", delete=False)
tmp.write(data)
tmp.close()
return tmp.name
except Exception:
return None
def download_file(url: str, depth: int = 0) -> Tuple[Optional[str], Optional[str]]:
"""[F21] Relative URLs resolved via urljoin."""
if depth > 3:
return None, "Download failed"
for headers in [
{"User-Agent": "Mozilla/5.0"},
{"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X)"},
]:
r = http_get(url, timeout=60, headers=headers)
if not r:
continue
content = r.content
if len(content) > MAX_FILE_SIZE:
return None, "File too large (>5 MB)"
head = content[:600].decode("utf-8", errors="ignore").lower()
if content[:2] == b"PK":
tmp = tempfile.NamedTemporaryFile(mode="wb", suffix=".zip", delete=False)
tmp.write(content)
tmp.close()
srt = extract_zip_srt(tmp.name)
with contextlib.suppress(OSError):
os.unlink(tmp.name)
return (srt, None) if srt else (None, "Invalid ZIP")
if "<html" in head or "<!doctype" in head:
soup = BeautifulSoup(content, "html.parser")
for a in soup.find_all("a", href=True):
href = a["href"]
if re.search(r"\.(srt|zip|vtt)(\?.*)?$", href, re.I):
resolved = urljoin(url, href) # [F21] FIX
return download_file(resolved, depth + 1)
continue
is_vtt = head.startswith("webvtt") or url.lower().endswith(".vtt") or "webvtt" in head[:80]
if is_vtt:
text = content.decode("utf-8", errors="ignore")
converted = vtt_to_srt(text)
if is_valid_srt(converted):
tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".srt", delete=False, encoding="utf-8")
tmp.write(converted)
tmp.close()
return tmp.name, None
continue
tmp = tempfile.NamedTemporaryFile(mode="wb", suffix=".srt", delete=False)
tmp.write(content)
tmp.close()
return tmp.name, None
return None, "Download failed"
def read_file_content(path: str) -> Optional[str]:
"""[F12] Min 100 chars guard."""
try:
with open(path, "r", encoding="utf-8", errors="ignore") as f:
c = f.read()
return c if len(c) > 100 else None
except Exception:
return None
async def send_subtitle_file(
bot, chat_id: int, path: str, filename: str,
caption: str, user_id: int, db: Database,
) -> bool:
try:
with open(path, "rb") as fp:
await bot.send_document(
chat_id, document=fp, filename=filename,
caption=caption, parse_mode="HTML",
)
await db.increment_downloads(user_id)
return True
except Exception as e:
logger.error(f"Send failed: {e}")
try:
await bot.send_message(chat_id, "⚠️ File send failed. Please try again.")
except Exception:
pass
return False
finally:
with contextlib.suppress(OSError):
os.unlink(path)
# ═══════════════════════════════════════════════════════════════
# SOURCES
# ═══════════════════════════════════════════════════════════════
async def _retry_source(func: Callable, q: str, retries: int = 2) -> List[dict]:
"""Retry a synchronous source function with backoff."""
for attempt in range(retries):
try:
result = await asyncio.to_thread(func, q)
if result:
return result
if attempt < retries - 1:
await asyncio.sleep(1)
except Exception as e:
logger.warning(f"{func.__name__} attempt {attempt + 1}: {e}")
if attempt == retries - 1:
return []
return []
def search_wyzie(q: str) -> List[dict]:
if not WYZIE_API_KEY or not OMDB_API_KEY:
return []
try:
clean_q = re.sub(r"\bS\d{1,2}E\d{1,2}\b", "", q, flags=re.I).strip()
r = http_get("https://www.omdbapi.com/", # [F15] HTTPS
params={"apikey": OMDB_API_KEY, "t": clean_q}, timeout=10)
if not r or r.json().get("Response") != "True":
return []
imdb_id = r.json().get("imdbID")
r2 = http_get("https://sub.wyzie.io/search",
params={"id": imdb_id, "source": "all", "key": WYZIE_API_KEY}, timeout=30)
if not r2 or not isinstance(r2.json(), list):
return []
results = []
for sub in r2.json():
lang = clean_language_name(sub.get("display", sub.get("language", "")))
dl = sub.get("url")
if not dl:
continue
results.append({
"_type": "wyzie", "title": f"{q} ({lang})",
"language": lang, "download": dl,
})
return results
except Exception as e:
logger.warning(f"Wyzie: {e}")
return []
def search_goat(q: str) -> List[dict]:
"""[F17] Uses _scrape_get (retry + session)."""
try:
r = _scrape_get(
f"https://malayalamsubtitles.in/search-and-download?search={quote_plus(q)}",
timeout=15,
)
if not r:
return []
soup = BeautifulSoup(r.text, "html.parser")
results, seen = [], set()
for a in soup.find_all("a", href=True):
href = a["href"]
if "/download/" in href and href not in seen:
title = a.get_text(strip=True)
if title and len(title) > 5 and title not in ["ഡൗൺലോഡ്", "Download"]:
results.append({
"_type": "goat", "title": title[:60],
"download": href, "language": "Malayalam",
})
seen.add(href)
return results[:10]
except Exception as e:
logger.warning(f"Team GOAT: {e}")
return []
def search_malsub_org(q: str) -> List[dict]:
try:
r = _scrape_get(f"https://malayalamsubtitles.org/?s={quote_plus(q)}", timeout=15)
if not r:
return []
soup = BeautifulSoup(r.text, "html.parser")
results = []
for a in soup.find_all("a", href=True):
href = a["href"]
if "?wpdmdl=" in href:
title = a.get_text(strip=True)
if title and len(title) > 3 and not any(
x in title.lower() for x in ["പരിഭാഷകൾ", "send", "submit"]
):
results.append({
"_type": "malsub", "title": title[:60],
"download": href, "language": "Malayalam",
})
return results[:10]
except Exception as e:
logger.warning(f"MalSub.org: {e}")
return []
def search_jerry(q: str) -> List[dict]:
try:
r = http_get(JERRY_SEARCH, params={"query": q}, timeout=15)
if r:
data = r.json()
if data.get("status") and isinstance(data.get("data"), list):
return [
{"_type": "jerry", "title": i.get("title", q)[:60], "url": i.get("url", "")}
for i in data["data"] if i.get("url")
]
except Exception as e:
logger.warning(f"Jerry: {e}")
return []
def get_jerry_langs(url: str) -> List[dict]:
try:
r = http_get(JERRY_DOWNLOAD, params={"url": url}, timeout=15)
if r:
data = r.json()
if data.get("status") and isinstance(data.get("data"), list):
return [
{"language": clean_language_name(l.get("language", "")), "download": l.get("url", "")}
for l in data["data"] if l.get("url")
]
except Exception as e:
logger.warning(f"Jerry langs: {e}")
return []
def search_ironman(q: str) -> List[dict]:
try:
r = http_get(IRON_SEARCH, params={"query": q}, timeout=15)
if r and isinstance(r.json(), list):
return [
{"_type": "ironman", "title": i.get("title", q)[:60], "url": i.get("url", "")}
for i in r.json() if i.get("url")
]
except Exception as e:
logger.warning(f"Ironman: {e}")
return []
def get_ironman_langs(url: str) -> List[dict]:
try:
r = http_get(IRON_DOWNLOAD, params={"url": url}, timeout=15)
if r and isinstance(r.json(), list):
return [
{"language": clean_language_name(l.get("language", "")), "download": l.get("url", "")}
for l in r.json()
]
except Exception as e:
logger.warning(f"Ironman langs: {e}")
return []
# ═══════════════════════════════════════════════════════════════
# TRANSLATION
# ═══════════════════════════════════════════════════════════════
def _deep_block(text: str, lang: str) -> str:
"""[F25] Chunks text >4800 chars instead of truncating."""
try:
translator = GoogleTranslator(source="auto", target=lang)
if len(text) <= 4800:
return translator.translate(text) or text
chunks = [text[i:i + 4800] for i in range(0, len(text), 4800)]
return "".join(translator.translate(c) or c for c in chunks)
except Exception:
return text
async def _gtx_batch(
client: httpx.AsyncClient,
sem: asyncio.Semaphore,
batch: List[str],
lang: str,
retries: int = 3,
) -> List[str]:
if not batch:
return []
url = "https://translate.googleapis.com/translate_a/single"
params = {"client": "gtx", "sl": "auto", "tl": lang, "dt": "t"}
data = [("q", t) for t in batch]
async with sem:
for attempt in range(retries):
try:
resp = await client.post(url, params=params, data=data, timeout=30.0)
if resp.status_code == 429:
await asyncio.sleep(2 ** attempt)
continue
resp.raise_for_status()
raw = resp.json()
out = [seg[0] for seg in raw[0] if seg and seg[0]]
if len(out) < len(batch):
out += batch[len(out):]
return out[: len(batch)]
except Exception:
if attempt == retries - 1:
return []
await asyncio.sleep(2 ** attempt)
return []
async def translate_srt_async(
content: str,
lang: str = "ml",
progress_msg: Optional[Message] = None,
) -> Optional[str]:
if not content or len(content) < 100:
return None
blocks = re.split(r"\n{2,}", content.strip())
if not blocks:
return None
texts = []
for block in blocks:
lines = block.strip().split("\n")
if len(lines) >= 3 and re.match(r"\d{1,2}:\d{2}:\d{2}[,.]", lines[1]):
texts.append("\n".join(lines[2:]))
else:
texts.append(block)
BATCH = 50
batches = [texts[i:i + BATCH] for i in range(0, len(texts), BATCH)]
total_b = len(batches)
sem = asyncio.Semaphore(10)
done = 0
last_edit = [0.0]
async def _run(client: httpx.AsyncClient, batch: List[str]) -> List[str]:
nonlocal done
res = await _gtx_batch(client, sem, batch, lang)
done += 1
if progress_msg:
now = asyncio.get_event_loop().time()
if now - last_edit[0] >= 1.5: # throttle edits
last_edit[0] = now
with contextlib.suppress(TelegramError):
await progress_msg.edit_text(
ui_translate_progress(done, total_b), parse_mode="HTML"
)
return res
async with httpx.AsyncClient() as client:
raw = await asyncio.gather(*[_run(client, b) for b in batches])
translated: List[str] = []
for batch, res in zip(batches, raw):
if res:
translated.extend(res)
else:
fb = await asyncio.gather(
*[asyncio.to_thread(_deep_block, t, lang) for t in batch]
)
translated.extend(fb)
while len(translated) < len(blocks):
translated.append(blocks[len(translated)])
out = []
for block, tr in zip(blocks, translated):
lines = block.strip().split("\n")
if len(lines) >= 3 and re.match(r"\d{1,2}:\d{2}:\d{2}[,.]", lines[1]):
out.append(f"{lines[0]}\n{lines[1]}\n{tr}")
else:
out.append(tr)
return "\n\n".join(out)
# ═══════════════════════════════════════════════════════════════
# HELPERS
# ═══════════════════════════════════════════════════════════════
async def _try_send(bot, chat_id: int, text: str) -> bool:
try:
await bot.send_message(chat_id=chat_id, text=text, parse_mode="HTML")
return True
except Exception as e:
logger.warning(f"Send {chat_id}: {e}")
return False
# ═══════════════════════════════════════════════════════════════
# BOT HANDLERS
# ═══════════════════════════════════════════════════════════════
class BotHandlers:
def __init__(self, db: Database):
self.db = db
self.search_cache: Dict[str, Tuple[Any, float]] = {}
self.cache_lock = asyncio.Lock()
# [F02] All asyncio primitives inside __init__ (inside event loop)
self._search_sem = asyncio.Semaphore(MAX_CONCURRENT_SEARCH)
self._translation_sem = asyncio.Semaphore(MAX_CONCURRENT_TRANSLATIONS)
self._membership_lock = asyncio.Lock()
self._membership_cache: Dict[int, Tuple[bool, float]] = {}
self._rate_lock = asyncio.Lock()
self._rate_last: Dict[int, float] = {}
# ── Rate limiter ─────────────────────────────────────────────
async def _is_limited(self, user_id: int) -> bool:
async with self._rate_lock:
now = time.time()
last = self._rate_last.get(user_id, 0)
if now - last < RATE_LIMIT_S:
return True
self._rate_last[user_id] = now
return False
async def _wait_sec(self, user_id: int) -> int:
async with self._rate_lock:
now = time.time()
last = self._rate_last.get(user_id, 0)
return max(1, int(RATE_LIMIT_S - (now - last)))
# ── Membership ───────────────────────────────────────────────
async def _check_membership(self, bot, user_id: int, force: bool = False) -> bool:
if user_id in ADMIN_IDS:
return True
now = time.time()
async with self._membership_lock:
if not force and user_id in self._membership_cache:
status, ts = self._membership_cache[user_id]
if now - ts < MEMBERSHIP_CACHE_TTL:
return status
try:
member = await bot.get_chat_member(GROUP_ID, user_id)
status = member.status in ("member", "administrator", "creator")
except Exception as e:
logger.warning(f"Membership check {user_id}: {e}")
status = True # fail-open
async with self._membership_lock:
self._membership_cache[user_id] = (status, now)
return status
# ── Bounded cache ────────────────────────────────────────────
async def _cache_set(self, key: str, value: Any):
"""[F18] LRU eviction when cache full."""
if len(self.search_cache) >= MAX_CACHE_ENTRIES:
oldest = min(self.search_cache, key=lambda k: self.search_cache[k][1])
del self.search_cache[oldest]
self.search_cache[key] = value
# ── Gate user ────────────────────────────────────────────────
async def gate_user(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> bool:
user = update.effective_user
if not user:
return False
if await self.db.is_banned(user.id):
if update.message:
await update.message.reply_text("🚫 Access revoked.")
return False
if await self.db.is_maintenance() and user.id not in ADMIN_IDS:
if update.message:
await update.message.reply_text(
"🔧 <b>Maintenance mode.</b>\n\nEngine updating. Please wait. 🙏",
parse_mode="HTML",
)
return False
if not await self._check_membership(ctx.bot, user.id):
if update.message:
# [F10] correct join-required message (not start screen)
await update.message.reply_text(
ui_join_required(user.first_name or "User"),
reply_markup=kb_join(),
parse_mode="HTML",
)
return False
return True
# ── Parallel search with live progress ──────────────────────
async def _search_all_live(self, q: str, status_msg: Message) -> Tuple:
"""[F09] Progress callback throttled. [F24] Uses instance semaphore."""
sources = [
("Wyzie", search_wyzie),
("Team GOAT",search_goat),
("MSone", search_malsub_org),
("SubScene", search_jerry),
("OpenSub", search_ironman),
]
results = [[] for _ in sources]
done_flag = [False] * len(sources)
edit_lock = asyncio.Lock()
last_edit = [0.0]
async def _update():
async with edit_lock:
now = asyncio.get_event_loop().time()
if now - last_edit[0] < 1.2:
return
last_edit[0] = now
lines = [
f"{'✅' if done_flag[i] else '⏳'} {name}\n"
for i, (name, _) in enumerate(sources)
]
with contextlib.suppress(TelegramError):
await status_msg.edit_text(
ui_searching(q, lines), parse_mode="HTML"
)
async def _run(idx: int, func: Callable):
async with self._search_sem:
res = await _retry_source(func, q)
results[idx] = res
done_flag[idx] = True
await _update()
await asyncio.gather(*[_run(i, fn) for i, (_, fn) in enumerate(sources)])
return tuple(results)
# ── Core search ──────────────────────────────────────────────
async def _run_search(
self, query: str, status_msg: Message,
ctx: ContextTypes.DEFAULT_TYPE, user_id: int,
):
ctx.user_data["movie_name"] = query
ctx.user_data["user_id"] = user_id
await self.db.track_trending(query)
cache_key = f"raw_{query.lower()}"
async with self.cache_lock:
cached = self.search_cache.get(cache_key)
need_fetch = not (cached and time.time() - cached[1] < CACHE_TTL)
if need_fetch:
fetched = await self._search_all_live(query, status_msg)
async with self.cache_lock:
await self._cache_set(cache_key, (fetched, time.time()))
wyzie, goat, malsub, jerry, ironman = fetched
else:
wyzie, goat, malsub, jerry, ironman = cached[0]
with contextlib.suppress(TelegramError):
await status_msg.edit_text(
ui_searching(query, ["⚡ Using cached results…\n"]),
parse_mode="HTML",
)
# Combine, rank, dedup
all_results = deduplicate(
rank_results(
[item for src in [wyzie, goat, malsub, jerry, ironman] for item in src],
query,
)
)[:60]
# [F08][F26] Malayalam fast-path with retry up to 3 sources
mal_items = [r for r in all_results if "malayalam" in r.get("language", "").lower()]
if mal_items:
with contextlib.suppress(TelegramError):
await status_msg.edit_text(
"🇮🇳 <b>Malayalam subtitle കിട്ടി!</b>\n📡 Download ചെയ്യുന്നു…",
parse_mode="HTML",
)
path = content = mal_item = None
for candidate in mal_items[:3]:
p, _ = await asyncio.to_thread(download_file, candidate["download"])
if p:
c = read_file_content(p)
if is_valid_srt(c or ""):
path, content, mal_item = p, c, candidate
break
with contextlib.suppress(OSError):
os.unlink(p)
if path and mal_item:
fname = make_filename(query, ".srt")
src_label = SOURCE_LABEL.get(mal_item["_type"], mal_item["_type"])
ctx.user_data["last_content"] = content
caption = (
f"🎬 <b>{escape(query)}</b>\n"
f"✦ Malayalam Subtitle\n<i>via {src_label}</i>"
)
sent = await send_subtitle_file(
ctx.bot, status_msg.chat_id, path, fname, caption, user_id, self.db
)
if sent:
await self.db.add_history(user_id, query, "Malayalam")
with contextlib.suppress(TelegramError):
await status_msg.delete()
lang = await self.db.get_user_pref(user_id, "default_lang", "ml")
if await self.db.get_user_pref(user_id, "auto_translate", False):
prog = await ctx.bot.send_message(
status_msg.chat_id, ui_translate_start(), parse_mode="HTML"
)
await self._run_translation(
ctx, status_msg.chat_id, prog, content, query, user_id, lang
)
else:
await ctx.bot.send_message(
status_msg.chat_id,
ui_sent(query),
reply_markup=kb_translate(),
parse_mode="HTML",
)
return
# all Malayalam attempts failed — fall to results list
if not all_results:
with contextlib.suppress(TelegramError):
await status_msg.edit_text(ui_not_found(query), parse_mode="HTML")
return
ctx.user_data["results"] = all_results
ctx.user_data["results_page"] = 0
kb, _, total = kb_results(all_results, 0, query)
with contextlib.suppress(TelegramError):
await status_msg.edit_text(
ui_results(query, total), reply_markup=kb, parse_mode="HTML"
)
# ── Translation runner ───────────────────────────────────────
async def _run_translation(
self,
ctx: ContextTypes.DEFAULT_TYPE,
chat_id: int,
progress_msg: Optional[Message],
content: str,
movie_name: str,
user_id: int,
lang: str = "ml", # [F06] respects user's default_lang
):
async with self._translation_sem:
try:
if not progress_msg:
progress_msg = await ctx.bot.send_message(
chat_id, ui_translate_start(), parse_mode="HTML"
)
translated = await asyncio.wait_for(
translate_srt_async(content, lang, progress_msg),
timeout=TRANSLATE_TIMEOUT,
)
if translated:
fname = make_filename(movie_name, ".srt")
with tempfile.NamedTemporaryFile(
mode="w", suffix=".srt", delete=False, encoding="utf-8"
) as tmp:
tmp.write(translated)
tmp_path = tmp.name
lang_name = LANG_MAP.get(lang, lang.upper())
caption = (
f"🎬 <b>{escape(movie_name)}</b>\n"
f"🌐 {lang_name} (Auto-Translated)\n"
f"<i>via MunaX Engine</i>\n"
f"<i>⚠️ Auto-translated — quality may vary</i>"
)
sent = await send_subtitle_file(
ctx.bot, chat_id, tmp_path, fname, caption, user_id, self.db
)
with contextlib.suppress(TelegramError):
await progress_msg.edit_text(
ui_translate_done() if sent else ui_translate_fail(),
parse_mode="HTML",
)
else:
with contextlib.suppress(TelegramError):
await progress_msg.edit_text(ui_translate_fail(), parse_mode="HTML")
except asyncio.TimeoutError:
with contextlib.suppress(TelegramError):
await progress_msg.edit_text(ui_translate_fail(), parse_mode="HTML")
except Exception as e:
logger.error(f"Translation error: {e}")
with contextlib.suppress(TelegramError):
await progress_msg.edit_text(ui_translate_fail(), parse_mode="HTML")
# ════════════════════════════════════════════════════════════
# USER COMMANDS
# ════════════════════════════════════════════════════════════
async def cmd_start(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
if not user or await self.db.is_banned(user.id):
return
await self.db.update_user(user.id, user.username, user.first_name)
if not await self._check_membership(ctx.bot, user.id):
await update.message.reply_text(
ui_join_required(user.first_name or "User"),
reply_markup=kb_join(), parse_mode="HTML",
)
return
info = await self.db.get_user_info(user.id)
downloads = info.get("downloads", 0) if info else 0
await update.message.reply_text(
ui_start(user.first_name or "User", downloads), parse_mode="HTML"
)
async def cmd_help(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not await self.gate_user(update, ctx):
return
# [F05] HTML parse_mode (not Markdown)
await update.message.reply_text(
"📖 <b>How to use MunaX Subs</b>\n\n"
"<b>1.</b> Sinima / series title type ചെയ്യൂ\n"
"<b>2.</b> 5 sources ഒരേ സമയം search ആകും\n"
"<b>3.</b> Malayalam subtitle → ഉടൻ download\n"
"<b>4.</b> ഇല്ലെങ്കിൽ → language select → translate\n\n"
"──────────────────────────────────────\n"
"<b>Tips:</b>\n"
" → English title use ചെയ്യൂ\n"
" → Year add ചെയ്യൂ: <code>Avatar 2009</code>\n"
" → Series: <code>Breaking Bad S01E05</code>\n\n"
"──────────────────────────────────────\n"
"<b>Commands</b>\n"
" /search <movie> – Manual search\n"
" /translate – Translate last subtitle\n"
" /cancel – Reset session\n"
" /history – Recent downloads\n"
" /mystats – Your stats\n"
" /leaderboard – Top downloaders\n"
" /trending – Popular searches\n"
" /request <title> – Request a subtitle\n"
" /prefs – Preferences\n"
" /donate – Support the bot\n"
" /group – Community link\n"
" /report <text> – Bug report",
parse_mode="HTML",
)
async def cmd_cancel(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not await self.gate_user(update, ctx):
return
ctx.user_data.clear()
await update.message.reply_text(
"✅ <b>Session reset.</b>\n\nSinima title type ചെയ്യൂ.",
parse_mode="HTML",
)
async def cmd_search(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not await self.gate_user(update, ctx):
return
user = update.effective_user
query = " ".join(ctx.args).strip() if ctx.args else ""
if not query:
await update.message.reply_text(
"🔍 <b>Usage:</b> <code>/search Movie Title</code>\n\n"
"<i>Or just type the title directly in chat.</i>",
parse_mode="HTML",
)
return
if not is_clean_query(query):
await update.message.reply_text("🚫 Query not allowed.")
return
if await self._is_limited(user.id):
w = await self._wait_sec(user.id)
await update.message.reply_text(f"⏳ Please wait <b>{w}s</b>.", parse_mode="HTML")
return
await self.db.update_user(user.id, user.username, user.first_name)
await update.effective_chat.send_action(constants.ChatAction.TYPING)
lines = [f"⏳ {n}\n" for n in ["Wyzie","Team GOAT","MSone","SubScene","OpenSub"]]
status = await update.message.reply_text(
ui_searching(query, lines), parse_mode="HTML"
)
await self._run_search(query, status, ctx, user.id)
async def cmd_translate(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not await self.gate_user(update, ctx):
return
user = update.effective_user
content = ctx.user_data.get("last_content")
if not content:
await update.message.reply_text(
"📭 Nothing to translate. Download a subtitle first."
)
return
if not is_valid_srt(content):
await update.message.reply_text(
"⚠️ Last file is not a valid subtitle. Download a subtitle first."
)
return
movie_name = ctx.user_data.get("movie_name", "subtitle")
chat_id = update.effective_chat.id
size_kb = len(content) / 1024
# [F06] fetch user's preferred language
lang = await self.db.get_user_pref(user.id, "default_lang", "ml")
ctx.user_data["_tr_chat_id"] = chat_id
ctx.user_data["_tr_movie_name"] = movie_name
ctx.user_data["_tr_lang"] = lang
ctx.user_data["_tr_content"] = content
if size_kb > 500:
await update.message.reply_text(
ui_large_file(size_kb), reply_markup=kb_large_file(), parse_mode="HTML"
)
return
prog = await update.message.reply_text(ui_translate_start(), parse_mode="HTML")
await self._run_translation(ctx, chat_id, prog, content, movie_name, user.id, lang)
async def cmd_history(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not await self.gate_user(update, ctx):
return
hist = await self.db.get_history(update.effective_user.id)
if not hist:
await update.message.reply_text(
"📋 <b>Download History</b>\n\nഇതുവരെ ഒന്നും ഇല്ല.\n\n<i>ഒരു title search ചെയ്യൂ!</i>",
parse_mode="HTML",
)
return
lines = []
for i, e in enumerate(hist, 1):
dt = datetime.fromtimestamp(e["t"]).strftime("%d %b")
src = f" <i>· {escape(e['src'])}</i>" if e.get("src") else ""
lines.append(f" <b>{i:02d}.</b> {escape(e['q'])}{src} <i>{dt}</i>")
await update.message.reply_text(
f"📋 <b>Your Archive</b>\n──────────────────────────────────────\n\n"
+ "\n".join(lines)
+ f"\n\n<i>Last {len(hist)} downloads.</i>",
parse_mode="HTML",
)
async def cmd_mystats(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not await self.gate_user(update, ctx):
return
uid = update.effective_user.id
info = await self.db.get_user_info(uid)
if not info:
await update.message.reply_text(
"📊 <b>Your Stats</b>\n\nDownloads ഒന്നും ഇല്ല.\n\n<i>ഒരു sinima search ചെയ്യൂ!</i>",
parse_mode="HTML",
)
return
first = datetime.fromtimestamp(info["first_seen"]).strftime("%d %b %Y")
dls = info.get("downloads", 0)
leaders = await self.db.get_leaderboard(50)
# [F23] compare by user_id
rank = next((i + 1 for i, (lid, _, _) in enumerate(leaders) if lid == uid), "?")
await update.message.reply_text(
f"📊 <b>Your Stats</b>\n──────────────────────────────────────\n\n"
f" 🎬 Downloads: <b>{dls}</b>\n"
f" 🏆 Rank: <b>#{rank}</b>\n"
f" 🕐 First visit: <i>{first}</i>",
parse_mode="HTML",
)
async def cmd_leaderboard(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not await self.gate_user(update, ctx):
return
leaders = await self.db.get_leaderboard(10)
if not leaders:
await update.message.reply_text(
"🏆 <b>Leaderboard</b>\n\n<i>ഇതുവരെ downloads ഒന്നും ഇല്ല.</i>",
parse_mode="HTML",
)
return
medals = ["🥇","🥈","🥉"] + ["🎬"] * 7
lines = [
f" {medals[i]} <b>{escape(name[:20])}</b> — <code>{dl}</code> downloads"
for i, (_, name, dl) in enumerate(leaders)
]
my_dls = (await self.db.get_user_info(update.effective_user.id) or {}).get("downloads", 0)
await update.message.reply_text(
"🏆 <b>Top Downloaders</b>\n──────────────────────────────────────\n\n"
+ "\n".join(lines)
+ f"\n\n<i>Yours: <b>{my_dls} downloads</b></i>",
parse_mode="HTML",
)
async def cmd_trending(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not await self.gate_user(update, ctx):
return
trends = await self.db.get_trending(10)
if not trends:
await update.message.reply_text(
"🔥 <b>Trending</b>\n\n<i>ഇതുവരെ data ഇല്ല.</i>", parse_mode="HTML"
)
return
lines = [
f" <b>{i+1:02d}.</b> {escape(q)} <i>({c}×)</i>"
for i, (q, c) in enumerate(trends)
]
await update.message.reply_text(
"🔥 <b>Trending Searches</b>\n──────────────────────────────────────\n\n"
+ "\n".join(lines)
+ "\n\n<i>This month's popular titles.</i>",
parse_mode="HTML",
)
async def cmd_request(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not await self.gate_user(update, ctx):
return
user = update.effective_user
title = " ".join(ctx.args).strip() if ctx.args else ""
if not title:
await update.message.reply_text(
"📥 <b>Subtitle Request</b>\n\n"
"<b>Usage:</b> <code>/request Movie Title</code>\n\n"
"<i>കിട്ടാത്ത subtitles request ചെയ്യൂ.</i>",
parse_mode="HTML",
)
return
if not is_clean_query(title):
await update.message.reply_text("🚫 Title not allowed.")
return
saved = await self.db.add_request(user.id, user.username or "", title)
if not saved:
await update.message.reply_text(
f"⚠️ Request limit ({MAX_REQUESTS_PER_USER}) reached.\n\n"
f"<i>Wait for previous requests to be processed.</i>",
parse_mode="HTML",
)
return
await update.message.reply_text(
f"📥 <b>Request saved!</b>\n\n"
f"<b>{escape(title)}</b> — recorded.\n"
f"Team check ചെയ്ത് available ആക്കും.\n\n"
f"<i>Updates: {GROUP_LINK}</i>",
parse_mode="HTML",
disable_web_page_preview=True,
)
admin_msg = (
f"📥 <b>New Request</b>\n"
f"──────────────────────────────────────\n"
f" 👤 <b>{escape(user.first_name or 'Unknown')}</b> (@{user.username or 'none'})\n"
f" 🎬 <b>{escape(title)}</b>\n"
f" 🕐 {datetime.now().strftime('%d %b %Y %H:%M')}"
)
for aid in ADMIN_IDS:
await _try_send(ctx.bot, aid, admin_msg)
async def cmd_donate(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not await self.gate_user(update, ctx):
return
await update.message.reply_text(
"💛 <b>Support MunaX Subs</b>\n──────────────────────────────────────\n\n"
"Real servers, real costs. ഒരു sinima save ആക്കിയിട്ടുണ്ടോ? ❤️\n\n"
f" 🏦 UPI: <code>{UPI_ID}</code>\n\n"
" 📸 <a href='https://www.instagram.com/munavi.r_'>Instagram</a>\n\n"
"ഓരോ rupee-യും matter ആകും. 🙏",
parse_mode="HTML",
disable_web_page_preview=True,
)
async def cmd_group(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(
"👥 <b>Malayalam Subtitle Hub</b>\n──────────────────────────────────────\n\n"
"Subtitle requests, bug reports, movie talk — everything.\n\n"
f"🔗 {GROUP_LINK}\n\n<i>Come join us!</i> 🎬",
parse_mode="HTML",
)
async def cmd_report(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not await self.gate_user(update, ctx):
return
user = update.effective_user
text = " ".join(ctx.args).strip() if ctx.args else ""
if not text:
await update.message.reply_text(
"🚨 <b>Bug / Problem Report</b>\n\n"
"<b>Usage:</b>\n<code>/report Kantara subtitle download ആകുന്നില്ല</code>",
parse_mode="HTML",
)
return
admin_msg = (
f"🚨 <b>User Report</b>\n"
f"──────────────────────────────────────\n"
f" 👤 <b>{escape(user.first_name or 'Unknown')}</b> (@{user.username or 'none'})\n"
f" 🆔 <code>{user.id}</code>\n"
f" 🕐 {datetime.now().strftime('%d %b %Y %H:%M')}\n\n"
f"{escape(text)}"
)
outcomes = await asyncio.gather(*[_try_send(ctx.bot, aid, admin_msg) for aid in ADMIN_IDS])
await update.message.reply_text(
"✅ <b>Report received.</b> Team-നെ അറിയിച്ചു. 🙏"
if any(outcomes) else
"⚠️ Deliver failed. Contact @munax9 directly.",
parse_mode="HTML",
)
async def cmd_feedback(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not await self.gate_user(update, ctx):
return
user = update.effective_user
fb = " ".join(ctx.args).strip() if ctx.args else ""
if not fb:
await update.message.reply_text(
"📝 <b>Send Feedback</b>\n\n<b>Usage:</b> <code>/feedback Your message</code>",
parse_mode="HTML",
)
return
admin_msg = (
f"💬 <b>Feedback</b>\n"
f"──────────────────────────────────────\n"
f" 👤 <b>{escape(user.first_name or 'Unknown')}</b> (@{user.username or 'none'})\n"
f" 🆔 <code>{user.id}</code>\n\n{escape(fb)}"
)
outcomes = await asyncio.gather(*[_try_send(ctx.bot, aid, admin_msg) for aid in ADMIN_IDS])
await update.message.reply_text(
"📨 Feedback sent. Thanks!" if any(outcomes) else "⚠️ Deliver failed.",
parse_mode="HTML",
)
async def cmd_prefs(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not await self.gate_user(update, ctx):
return
user = update.effective_user
args = ctx.args or []
if not args:
auto = await self.db.get_user_pref(user.id, "auto_translate", False)
lang = await self.db.get_user_pref(user.id, "default_lang", "ml")
await update.message.reply_text(
"⚙️ <b>Preferences</b>\n──────────────────────────────────────\n\n"
f" 🔁 Auto-translate: <b>{'On' if auto else 'Off'}</b>\n"
f" 🌐 Default lang: <b>{LANG_MAP.get(lang, lang)}</b>\n\n"
"<b>Change:</b>\n"
" /prefs auto on|off\n"
" /prefs lang <code>ml|en|hi|ta|…</code>",
parse_mode="HTML",
)
return
if args[0] == "auto":
if len(args) < 2 or args[1] not in ("on", "off"):
await update.message.reply_text("Usage: /prefs auto on|off")
return
await self.db.set_user_pref(user.id, "auto_translate", args[1] == "on")
await update.message.reply_text("✅ Preferences updated.", parse_mode="HTML")
elif args[0] == "lang":
if len(args) < 2 or args[1] not in LANG_MAP:
await update.message.reply_text(
f"Usage: /prefs lang <code>{'|'.join(list(LANG_MAP.keys())[:12])}|…</code>",
parse_mode="HTML",
)
return
await self.db.set_user_pref(user.id, "default_lang", args[1])
await update.message.reply_text("✅ Preferences updated.", parse_mode="HTML")
else:
await update.message.reply_text("Unknown option. Use /prefs auto|lang")
# ════════════════════════════════════════════════════════════
# ADMIN COMMANDS — all fully implemented [F01]
# ════════════════════════════════════════════════════════════
async def cmd_admin(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if update.effective_user.id not in ADMIN_IDS:
await update.message.reply_text("🚫 Access denied.")
return
await update.message.reply_text(
"🛡️ <b>Admin Panel</b>\n──────────────────────────────────────\n\n"
"/stats – Bot statistics\n"
"/ping – Response time\n"
"/broadcast <msg> – Message all users\n"
"/ban <id> – Ban user\n"
"/unban <id> – Unban user\n"
"/banned – List banned users\n"
"/requests – Pending requests\n"
"/donereq <title> – Mark request done\n"
"/userinfo <id> – User details\n"
"/maintenance on|off – Toggle maintenance\n"
"/clearcache – Clear search cache",
parse_mode="HTML",
)
async def cmd_stats(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if update.effective_user.id not in ADMIN_IDS:
return
total_users, total_dls = await self.db.get_total_stats()
banned = len(await self.db.get_banned_users())
groups = len(await self.db.get_all_groups())
async with self.cache_lock:
cache_sz = len(self.search_cache)
maint = await self.db.is_maintenance()
await update.message.reply_text(
f"📊 <b>Engine Status</b>\n──────────────────────────────────────\n\n"
f" 👥 Users: <b>{total_users:,}</b>\n"
f" 📥 Downloads: <b>{total_dls:,}</b>\n"
f" 🚫 Banned: <b>{banned}</b>\n"
f" 👥 Groups: <b>{groups}</b>\n"
f" 🗃️ Cache: <b>{cache_sz} entries</b>\n"
f" 🔧 Maintenance: <b>{'On' if maint else 'Off'}</b>\n\n"
f"<i>{pb('MUNAX SUBS')} ☠️ {VERSION}</i>",
parse_mode="HTML",
)
async def cmd_ping(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if update.effective_user.id not in ADMIN_IDS:
return
start = time.time()
msg = await update.message.reply_text("🏓 Checking…")
ms = int((time.time() - start) * 1000)
await msg.edit_text(
f"🏓 <b>Engine alive.</b> {ms} ms\n<i>All systems operational.</i>",
parse_mode="HTML",
)
async def cmd_broadcast(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if update.effective_user.id not in ADMIN_IDS:
return
text = " ".join(ctx.args).strip() if ctx.args else ""
if not text:
await update.message.reply_text("Usage: /broadcast Your message here")
return
users = await self.db.get_all_user_ids()
groups = await self.db.get_all_groups()
total = len(users) + len(groups)
status = await update.message.reply_text(
f"📡 Broadcasting to <b>{len(users)}</b> users + <b>{len(groups)}</b> groups…",
parse_mode="HTML",
)
sem = asyncio.Semaphore(20)
async def _send(chat_id: int) -> bool:
async with sem:
result = await _try_send(ctx.bot, chat_id, text)
await asyncio.sleep(0.05) # [F22] flood-safe
return result
outcomes = await asyncio.gather(*[_send(i) for i in users + groups])
sent = sum(outcomes)
await status.edit_text(
f"📡 <b>Broadcast complete.</b>\n\n"
f" ✅ Delivered: <b>{sent}</b>\n"
f" ☠️ Failed: <b>{total - sent}</b>",
parse_mode="HTML",
)
async def cmd_ban(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if update.effective_user.id not in ADMIN_IDS:
return
if not ctx.args:
await update.message.reply_text("Usage: /ban <user_id>")
return
try:
uid = int(ctx.args[0])
except ValueError:
await update.message.reply_text("❌ Invalid user ID.")
return
await self.db.ban_user(uid)
await update.message.reply_text(
f"🚫 <code>{uid}</code> — banned.", parse_mode="HTML"
)
async def cmd_unban(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if update.effective_user.id not in ADMIN_IDS:
return
if not ctx.args:
await update.message.reply_text("Usage: /unban <user_id>")
return
try:
uid = int(ctx.args[0])
except ValueError:
await update.message.reply_text("❌ Invalid user ID.")
return
await self.db.unban_user(uid)
await update.message.reply_text(
f"✅ <code>{uid}</code> — unbanned.", parse_mode="HTML"
)
async def cmd_banned(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if update.effective_user.id not in ADMIN_IDS:
return
banned = await self.db.get_banned_users()
if not banned:
await update.message.reply_text("✅ No banned users.")
return
lines = []
for uid in banned:
info = await self.db.get_user_info(uid)
uname = info.get("username", "?") if info else "?"
fname = info.get("first_name", "?") if info else "?"
lines.append(f" ☠️ <code>{uid}</code> @{escape(uname)} {escape(fname)}")
await update.message.reply_text(
f"🚫 <b>Banned ({len(banned)})</b>\n──────────────────────────────────────\n"
+ "\n".join(lines),
parse_mode="HTML",
)
async def cmd_requests(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if update.effective_user.id not in ADMIN_IDS:
return
reqs = await self.db.get_pending_requests(20)
if not reqs:
await update.message.reply_text("✅ No pending requests.")
return
lines = []
for r in reqs:
dt = datetime.fromtimestamp(r["t"]).strftime("%d %b")
uname = f"@{r['username']}" if r.get("username") else str(r.get("uid","?"))
lines.append(f" 🎬 <b>{escape(r['title'])}</b> <i>— {uname} {dt}</i>")
await update.message.reply_text(
f"📥 <b>Pending Requests ({len(reqs)})</b>\n──────────────────────────────────────\n\n"
+ "\n".join(lines)
+ "\n\n<i>/donereq <title> to mark done</i>",
parse_mode="HTML",
)
async def cmd_donereq(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if update.effective_user.id not in ADMIN_IDS:
return
title = " ".join(ctx.args).strip() if ctx.args else ""
if not title:
await update.message.reply_text("Usage: /donereq Movie Title")
return
cnt = await self.db.mark_request_done(title)
await update.message.reply_text(
f"✅ <b>{escape(title)}</b> — {cnt} request(s) marked done.",
parse_mode="HTML",
)
async def cmd_userinfo(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if update.effective_user.id not in ADMIN_IDS:
return
if not ctx.args:
await update.message.reply_text("Usage: /userinfo <user_id>")
return
try:
uid = int(ctx.args[0])
except ValueError:
await update.message.reply_text("❌ Invalid user ID.")
return
info = await self.db.get_user_info(uid)
if not info:
await update.message.reply_text(
f"ℹ️ No data for <code>{uid}</code>.", parse_mode="HTML"
)
return
banned = await self.db.is_banned(uid)
hist = await self.db.get_history(uid)
first = datetime.fromtimestamp(info["first_seen"]).strftime("%d %b %Y %H:%M")
recent = "\n".join(f" · {escape(h['q'])}" for h in hist[:5]) or " (none)"
await update.message.reply_text(
f"👤 <b>User Info</b>\n──────────────────────────────────────\n\n"
f" 🆔 <code>{uid}</code>\n"
f" 👤 {escape(info.get('first_name',''))} @{escape(info.get('username',''))}\n"
f" 📥 Downloads: <b>{info.get('downloads',0)}</b>\n"
f" 🕐 First seen: <i>{first}</i>\n"
f" 🚫 Banned: <b>{'Yes' if banned else 'No'}</b>\n\n"
f"<b>Recent searches:</b>\n{recent}",
parse_mode="HTML",
)
async def cmd_maintenance(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if update.effective_user.id not in ADMIN_IDS:
return
arg = (ctx.args[0].lower() if ctx.args else "")
if arg not in ("on","off"):
await update.message.reply_text("Usage: /maintenance on|off")
return
await self.db.set_maintenance(arg == "on")
await update.message.reply_text(
f"🔧 Maintenance: <b>{'On' if arg == 'on' else 'Off'}</b>",
parse_mode="HTML",
)
async def cmd_clearcache(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if update.effective_user.id not in ADMIN_IDS:
return
async with self.cache_lock:
cnt = len(self.search_cache)
self.search_cache.clear()
await update.message.reply_text(
f"🗃️ Cache cleared. <b>{cnt} entries</b> removed.", parse_mode="HTML"
)
# ════════════════════════════════════════════════════════════
# MESSAGE HANDLER
# ════════════════════════════════════════════════════════════
async def handle_message(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
msg = update.message
chat = update.effective_chat
if not user or user.is_bot:
return
if not await self.gate_user(update, ctx):
return
text = (msg.text or "").strip()
if not text:
return
is_group = chat.type in ("group","supergroup")
bot_username = ctx.bot.username or ctx.bot_data.get("username","")
mentioned = bool(bot_username and re.search(rf"@{re.escape(bot_username)}", text, re.I))
clean_text = re.sub(rf"@{re.escape(bot_username)}", "", text, flags=re.I).strip() if bot_username else text
if not clean_text:
clean_text = text
if is_group:
await self.db.add_group(chat.id)
if not mentioned and not is_movie_query(clean_text):
return
else:
if not is_movie_query(clean_text):
return
if not is_clean_query(clean_text):
return
if await self._is_limited(user.id):
w = await self._wait_sec(user.id)
await msg.reply_text(f"⏳ Please wait <b>{w}s</b>.", parse_mode="HTML")
return
await self.db.update_user(user.id, user.username, user.first_name)
await chat.send_action(constants.ChatAction.TYPING)
lines = [f"⏳ {n}\n" for n in ["Wyzie","Team GOAT","MSone","SubScene","OpenSub"]]
status = await msg.reply_text(ui_searching(clean_text, lines), parse_mode="HTML")
await self._run_search(clean_text, status, ctx, user.id)
# ════════════════════════════════════════════════════════════
# INLINE QUERY [F27]
# ════════════════════════════════════════════════════════════
async def inline_query(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
query = update.inline_query.query.strip()
user_id = update.inline_query.from_user.id
if not query or len(query) < 2 or not is_clean_query(query):
await update.inline_query.answer([], cache_time=0)
return
if await self.db.is_banned(user_id):
await update.inline_query.answer([], cache_time=0)
return
cache_key = f"raw_{query.lower()}"
raw_items: List[dict] = []
async with self.cache_lock:
cached = self.search_cache.get(cache_key)
if cached and time.time() - cached[1] < CACHE_TTL:
raw_items = [item for src in cached[0] for item in src]
bot_uname = ctx.bot.username or ""
if not raw_items:
await update.inline_query.answer([
InlineQueryResultArticle(
id=str(uuid.uuid4()),
title=f"🔍 Search: {query}",
description="Bot-ൽ type ചെയ്ത് full results കാണൂ",
input_message_content=InputTextMessageContent(
f"🎬 <b>{escape(query)}</b> subtitle — @{bot_uname}-ൽ search ചെയ്യൂ",
parse_mode="HTML",
),
)
], cache_time=0)
return
ranked = deduplicate(rank_results(raw_items, query))[:5]
answers = []
for r in ranked:
title = r.get("title", query)[:60]
lang = r.get("language","Unknown")
src = SOURCE_LABEL.get(r.get("_type",""), "Unknown")
badge = "🇮🇳" if "malayalam" in lang.lower() else "🎬"
answers.append(InlineQueryResultArticle(
id=str(uuid.uuid4()),
title=f"{badge} {title}",
description=f"{lang} · {src}",
input_message_content=InputTextMessageContent(
f"🎬 <b>{escape(title)}</b>\n\nDownload: @{bot_uname}",
parse_mode="HTML",
),
))
await update.inline_query.answer(answers, cache_time=60)
# ════════════════════════════════════════════════════════════
# CALLBACK HANDLER
# ════════════════════════════════════════════════════════════
async def handle_callback(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
q = update.callback_query
user = q.from_user
if await self.db.is_banned(user.id):
await q.answer("🚫 Access revoked.", show_alert=True)
return
if await self.db.is_maintenance() and user.id not in ADMIN_IDS:
await q.answer("🔧 Maintenance mode. Try later.", show_alert=True)
return
if not await self._check_membership(ctx.bot, user.id):
await q.answer("Group join ചെയ്യൂ first.", show_alert=True)
return
try:
await q.answer()
except TelegramError:
return
data = q.data
chat_id = q.message.chat_id
user_id = ctx.user_data.get("user_id", user.id)
movie_name = ctx.user_data.get("movie_name", "Movie")
cur_page = ctx.user_data.get("results_page", 0)
# ── simple actions ──────────────────────────────────────
if data == "noop":
return
if data == "new_search":
ctx.user_data.clear()
await q.message.edit_text(
"🔍 <b>Ready!</b>\n\nSinima / series title type ചെയ്യൂ.\n\n"
"<i>Example:</i> <code>Kantara</code> · <code>Dune 2021</code> · <code>BB S01E05</code>",
parse_mode="HTML",
)
return
if data == "verify":
if await self._check_membership(ctx.bot, user.id, force=True):
await q.message.edit_text(
"✅ <b>Verified!</b> Welcome! 🎬\n\nSinima title type ചെയ്യൂ.",
parse_mode="HTML",
)
else:
await q.message.edit_text(
"❌ <b>Not joined yet.</b>\n\nGroup join ചെയ്ത് verify ചെയ്യൂ.",
parse_mode="HTML",
)
return
if data == "skip":
await q.message.edit_text(
"✅ <b>OK!</b> Subtitle ചാറ്റിൽ ഉണ്ട്.\n\nAnotherSinima title type ചെയ്യൂ.",
parse_mode="HTML",
)
return
if data in ("translate","translate_force"):
content = ctx.user_data.get("last_content") or ctx.user_data.get("_tr_content")
if not content:
await q.message.edit_text(
"📭 Nothing to translate. Download a subtitle first."
)
return
_chat_id = ctx.user_data.get("_tr_chat_id", chat_id)
_movie_name = ctx.user_data.get("_tr_movie_name", movie_name)
_user_id = ctx.user_data.get("_tr_user_id", user_id)
lang = ctx.user_data.get("_tr_lang") or await self.db.get_user_pref(user_id, "default_lang", "ml")
size_kb = len(content) / 1024
if data == "translate" and size_kb > 500:
await q.message.edit_text(
ui_large_file(size_kb), reply_markup=kb_large_file(), parse_mode="HTML"
)
ctx.user_data["_tr_content"] = content
ctx.user_data["_tr_chat_id"] = _chat_id
ctx.user_data["_tr_movie_name"] = _movie_name
ctx.user_data["_tr_user_id"] = _user_id
ctx.user_data["_tr_lang"] = lang
return
await q.message.edit_text(ui_translate_start(), parse_mode="HTML")
await self._run_translation(
ctx, _chat_id, q.message, content, _movie_name, _user_id, lang
)
return
if data.startswith("pg_"):
page = int(data[3:])
ctx.user_data["results_page"] = page
results = ctx.user_data.get("results", [])
kb, _, _ = kb_results(results, page, movie_name)
with contextlib.suppress(TelegramError):
await q.message.edit_reply_markup(reply_markup=kb)
return
# ── result selection ────────────────────────────────────
if data.startswith("sub_"):
idx = int(data[4:])
results = ctx.user_data.get("results", [])
if idx >= len(results):
await q.message.edit_text(
"⏱ <b>Session expired.</b>\n\nSinima name വീണ്ടും type ചെയ്യൂ.",
parse_mode="HTML",
)
return
sel = results[idx]
typ = sel["_type"]
title = sel.get("title", movie_name)
src_label = SOURCE_LABEL.get(typ, typ)
ctx.user_data["selected_title"] = title
# Direct-download sources
if typ in ("wyzie","goat","malsub"):
await q.message.edit_text(ui_downloading(), parse_mode="HTML")
path, err = await asyncio.to_thread(download_file, sel["download"])
if path:
content = read_file_content(path)
if is_valid_srt(content or ""):
fname = make_filename(title, ".srt")
lang_name = sel.get("language","Subtitle")
ctx.user_data["last_content"] = content
caption = (
f"🎬 <b>{escape(title)}</b>\n"
f"✦ {lang_name}\n<i>via {src_label}</i>"
)
sent = await send_subtitle_file(
ctx.bot, chat_id, path, fname, caption, user_id, self.db
)
if sent:
await self.db.add_history(
user_id, title, f"{lang_name} · {src_label}"
)
lang = await self.db.get_user_pref(user_id, "default_lang", "ml")
if await self.db.get_user_pref(user_id, "auto_translate", False):
await q.message.edit_text(
ui_translate_start(), parse_mode="HTML"
)
await self._run_translation(
ctx, chat_id, q.message, content, title, user_id, lang
)
else:
await q.message.edit_text(
ui_sent(title),
reply_markup=kb_translate(),
parse_mode="HTML",
)
return
else:
with contextlib.suppress(OSError):
os.unlink(path)
await q.message.edit_text(
f"☠️ <b>{err or 'Download failed.'}</b>\n\nMറ്റൊരു result try ചെയ്യൂ.",
reply_markup=kb_back(cur_page),
parse_mode="HTML",
)
return
# Jerry / Ironman — need language selection
if typ in ("jerry","ironman"):
get_langs = get_jerry_langs if typ == "jerry" else get_ironman_langs
await q.message.edit_text(
f"🔎 <b>{escape(title[:50])}</b>\n\nAvailable languages check ചെയ്യുന്നു…",
parse_mode="HTML",
)
langs = await asyncio.to_thread(get_langs, sel["url"])
if not langs:
await q.message.edit_text(
"☠️ <b>No languages found.</b>\n\nMറ്റൊരു result try ചെയ്യൂ.",
reply_markup=kb_back(cur_page),
parse_mode="HTML",
)
return
mal = next(
(l for l in langs if "malayalam" in l.get("language","").lower()), None
)
oth = [l for l in langs if "malayalam" not in l.get("language","").lower()]
if mal:
await q.message.edit_text(
"🇮🇳 <b>Malayalam കിട്ടി!</b> Download ചെയ്യുന്നു…",
parse_mode="HTML",
)
path, err = await asyncio.to_thread(download_file, mal["download"])
if path:
content = read_file_content(path)
if is_valid_srt(content or ""):
fname = make_filename(title, ".srt")
ctx.user_data["last_content"] = content
caption = (
f"🎬 <b>{escape(title)}</b>\n"
f"✦ Malayalam Subtitle\n<i>via {src_label}</i>"
)
sent = await send_subtitle_file(
ctx.bot, chat_id, path, fname, caption, user_id, self.db
)
if sent:
await self.db.add_history(user_id, title, src_label)
lang = await self.db.get_user_pref(user_id, "default_lang", "ml")
if await self.db.get_user_pref(user_id, "auto_translate", False):
await q.message.edit_text(
ui_translate_start(), parse_mode="HTML"
)
await self._run_translation(
ctx, chat_id, q.message, content, title, user_id, lang
)
else:
await q.message.edit_text(
ui_sent(title),
reply_markup=kb_translate(),
parse_mode="HTML",
)
return
else:
with contextlib.suppress(OSError):
os.unlink(path)
await q.message.edit_text(
f"☠️ <b>{err or 'Download failed.'}</b>",
reply_markup=kb_back(cur_page),
parse_mode="HTML",
)
return
if oth:
kb_l, sl = kb_langs(oth[:12], cur_page)
ctx.user_data["pending_langs"] = [
{"language": l["language"], "download": l["download"], "_src": typ}
for l in sl
]
await q.message.edit_text(
f"🎬 <b>{escape(title[:50])}</b>\n\n"
f"🇮🇳 Malayalam ഇല്ല. ഒരു language select ചെയ്യൂ — translate ചെയ്യാം:",
reply_markup=kb_l,
parse_mode="HTML",
)
else:
await q.message.edit_text(
"☠️ <b>No languages found.</b>",
reply_markup=kb_back(cur_page),
parse_mode="HTML",
)
return
# ── language download ───────────────────────────────────
if data.startswith("lang_"):
idx = int(data[5:])
langs = ctx.user_data.get("pending_langs", [])
title = ctx.user_data.get("selected_title", movie_name)
if idx >= len(langs):
await q.message.edit_text(
"⏱ <b>Session expired.</b>\n\nSinima name വീണ്ടും type ചെയ്യൂ.",
parse_mode="HTML",
)
return
li = langs[idx]
lang_name = li["language"]
src_label = SOURCE_LABEL.get(li.get("_src",""), li.get("_src",""))
await q.message.edit_text(
f"📡 <b>{escape(lang_name)}</b> download ചെയ്യുന്നു…", parse_mode="HTML"
)
path, err = await asyncio.to_thread(download_file, li["download"])
if not path:
await q.message.edit_text(
f"☠️ <b>{err or 'Download failed.'}</b>",
reply_markup=kb_back(cur_page),
parse_mode="HTML",
)
return
content = read_file_content(path)
if not is_valid_srt(content or ""):
with contextlib.suppress(OSError):
os.unlink(path)
await q.message.edit_text(
"⚠️ <b>File not valid.</b>\n\nMറ്റൊരു result try ചെയ്യൂ.",
reply_markup=kb_back(cur_page),
parse_mode="HTML",
)
return
fname = make_filename(title, ".srt")
ctx.user_data["last_content"] = content
caption = (
f"🎬 <b>{escape(title)}</b>\n"
f"✦ {lang_name}\n<i>via {src_label}</i>"
)
sent = await send_subtitle_file(
ctx.bot, chat_id, path, fname, caption, user_id, self.db
)
if sent:
await self.db.add_history(user_id, title, f"{lang_name} · {src_label}")
lang = await self.db.get_user_pref(user_id, "default_lang", "ml")
if await self.db.get_user_pref(user_id, "auto_translate", False):
await q.message.edit_text(ui_translate_start(), parse_mode="HTML")
await self._run_translation(
ctx, chat_id, q.message, content, title, user_id, lang
)
else:
await q.message.edit_text(
ui_sent(title), reply_markup=kb_translate(), parse_mode="HTML"
)
# ═══════════════════════════════════════════════════════════════
# HEALTH SERVER
# ═══════════════════════════════════════════════════════════════
class HealthHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-type","application/json")
self.end_headers()
self.wfile.write(
json.dumps({"status":"ok","timestamp":datetime.utcnow().isoformat()}).encode()
)
def log_message(self, *a, **kw):
pass
def run_health(port: int):
HTTPServer(("0.0.0.0", port), HealthHandler).serve_forever()
# ═══════════════════════════════════════════════════════════════
# MAIN
# ═══════════════════════════════════════════════════════════════
async def post_init(app: Application):
"""[F13] db.init() runs here — correct event loop."""
db = app.bot_data["db"]
await db.init()
me = await app.bot.get_me()
app.bot_data["username"] = me.username
logger.info(f"✅ Engine live: @{me.username}")
if not WYZIE_API_KEY:
logger.warning("WYZIE_API_KEY not set — Wyzie disabled")
if not OMDB_API_KEY:
logger.warning("OMDB_API_KEY not set — OMDB disabled")
async def post_stop(app: Application):
"""[F20] Graceful DB close."""
db = app.bot_data.get("db")
if db:
await db.close()
logger.info("✅ Database closed cleanly.")
def main():
port = int(os.environ.get("PORT", 8080))
use_webhook = bool(RENDER_URL)
health_port = port + 1 if use_webhook else port
threading.Thread(target=run_health, args=(health_port,), daemon=True).start()
logger.info(f"Health server on port {health_port}")
db = Database()
app = (
Application.builder()
.token(BOT_TOKEN)
.post_init(post_init)
.post_stop(post_stop)
.build()
)
app.bot_data["db"] = db
h = BotHandlers(db)
# ── User commands ──────────────────────────────────────────
app.add_handler(CommandHandler("start", h.cmd_start))
app.add_handler(CommandHandler("help", h.cmd_help))
app.add_handler(CommandHandler("cancel", h.cmd_cancel))
app.add_handler(CommandHandler("search", h.cmd_search))
app.add_handler(CommandHandler("translate", h.cmd_translate))
app.add_handler(CommandHandler("history", h.cmd_history))
app.add_handler(CommandHandler("mystats", h.cmd_mystats))
app.add_handler(CommandHandler("leaderboard", h.cmd_leaderboard))
app.add_handler(CommandHandler("trending", h.cmd_trending))
app.add_handler(CommandHandler("request", h.cmd_request))
app.add_handler(CommandHandler("donate", h.cmd_donate))
app.add_handler(CommandHandler("group", h.cmd_group))
app.add_handler(CommandHandler("report", h.cmd_report))
app.add_handler(CommandHandler("feedback", h.cmd_feedback))
app.add_handler(CommandHandler("prefs", h.cmd_prefs))
# ── Admin commands ─────────────────────────────────────────
app.add_handler(CommandHandler("admin", h.cmd_admin))
app.add_handler(CommandHandler("stats", h.cmd_stats))
app.add_handler(CommandHandler("ping", h.cmd_ping))
app.add_handler(CommandHandler("broadcast", h.cmd_broadcast))
app.add_handler(CommandHandler("ban", h.cmd_ban))
app.add_handler(CommandHandler("unban", h.cmd_unban))
app.add_handler(CommandHandler("banned", h.cmd_banned))
app.add_handler(CommandHandler("requests", h.cmd_requests))
app.add_handler(CommandHandler("donereq", h.cmd_donereq))
app.add_handler(CommandHandler("userinfo", h.cmd_userinfo))
app.add_handler(CommandHandler("maintenance", h.cmd_maintenance))
app.add_handler(CommandHandler("clearcache", h.cmd_clearcache))
# ── Message + Callback + Inline ───────────────────────────
app.add_handler(
MessageHandler(filters.TEXT & ~filters.COMMAND, h.handle_message)
)
app.add_handler(
CallbackQueryHandler(
h.handle_callback,
pattern=r"^(noop|new_search|verify|translate|translate_force|skip|sub_\d+|pg_\d+|lang_\d+)$",
)
)
app.add_handler(InlineQueryHandler(h.inline_query))
app.add_error_handler(
lambda u, c: logger.error("Unhandled error", exc_info=c.error)
)
logger.info("━" * 50)
logger.info(f" {pb('MUNAX SUBS')} ☠️ {VERSION}")
logger.info("━" * 50)
if use_webhook:
logger.info(f"Mode: WEBHOOK → {RENDER_URL}")
app.run_webhook(
listen="0.0.0.0",
port=port,
url_path=BOT_TOKEN,
webhook_url=f"{RENDER_URL}/{BOT_TOKEN}",
drop_pending_updates=True,
)
else:
logger.info("Mode: POLLING")
app.run_polling(drop_pending_updates=True)
if __name__ == "__main__":
main()
================================================
FILE: requirements.txt
================================================
python-telegram-bot[webhooks]>=20.7
requests>=2.31.0
httpx>=0.25.0
beautifulsoup4>=4.12.0
deep-translator>=1.11.0
lxml>=4.9.0
aiosqlite>=0.19.0
gitextract_ds0fbhii/ ├── .gitignore ├── README.md ├── bot.py └── requirements.txt
SYMBOL INDEX (124 symbols across 1 files)
FILE: bot.py
function pb (line 163) | def pb(text: str) -> str:
function bar (line 171) | def bar(frac: float, w: int = 18) -> str:
function ui_box (line 175) | def ui_box(icon: str, title: str, body: str) -> str:
function ui_start (line 183) | def ui_start(name: str, downloads: int = 0) -> str:
function ui_join_required (line 199) | def ui_join_required(name: str) -> str:
function ui_searching (line 206) | def ui_searching(q: str, lines: List[str]) -> str:
function ui_results (line 216) | def ui_results(q: str, total: int) -> str:
function ui_not_found (line 223) | def ui_not_found(q: str) -> str:
function ui_downloading (line 234) | def ui_downloading() -> str:
function ui_sent (line 239) | def ui_sent(title: str) -> str:
function ui_translate_start (line 247) | def ui_translate_start() -> str:
function ui_translate_progress (line 253) | def ui_translate_progress(done: int, total: int) -> str:
function ui_translate_done (line 263) | def ui_translate_done() -> str:
function ui_translate_fail (line 271) | def ui_translate_fail() -> str:
function ui_large_file (line 274) | def ui_large_file(size_kb: float) -> str:
function kb_join (line 283) | def kb_join() -> InlineKeyboardMarkup:
function kb_translate (line 289) | def kb_translate() -> InlineKeyboardMarkup:
function kb_back (line 295) | def kb_back(page: int) -> InlineKeyboardMarkup:
function kb_large_file (line 300) | def kb_large_file() -> InlineKeyboardMarkup:
function kb_results (line 306) | def kb_results(results: List[dict], page: int, query: str) -> Tuple[Inli...
function kb_langs (line 328) | def kb_langs(langs: List[dict], page: int) -> Tuple[InlineKeyboardMarkup...
class Database (line 351) | class Database:
method __init__ (line 352) | def __init__(self, path: str = "bot_data.db"):
method init (line 356) | async def init(self):
method close (line 408) | async def close(self):
method update_user (line 413) | async def update_user(self, user_id: int, username: str, first_name: s...
method add_history (line 421) | async def add_history(self, user_id: int, query: str, source: str = ""):
method get_history (line 434) | async def get_history(self, user_id: int) -> List[Dict]:
method increment_downloads (line 441) | async def increment_downloads(self, user_id: int):
method get_user_info (line 447) | async def get_user_info(self, user_id: int) -> Optional[Dict]:
method is_banned (line 458) | async def is_banned(self, user_id: int) -> bool:
method ban_user (line 463) | async def ban_user(self, user_id: int):
method unban_user (line 467) | async def unban_user(self, user_id: int):
method get_banned_users (line 471) | async def get_banned_users(self) -> List[int]:
method add_group (line 475) | async def add_group(self, group_id: int):
method get_all_groups (line 481) | async def get_all_groups(self) -> List[int]:
method track_trending (line 485) | async def track_trending(self, query: str):
method get_trending (line 498) | async def get_trending(self, limit: int = 10) -> List[Tuple[str, int]]:
method add_request (line 504) | async def add_request(self, user_id: int, username: str, title: str) -...
method get_pending_requests (line 517) | async def get_pending_requests(self, limit: int = 20) -> List[Dict]:
method mark_request_done (line 528) | async def mark_request_done(self, title: str) -> int:
method get_leaderboard (line 535) | async def get_leaderboard(self, limit: int = 10) -> List[Tuple[int, st...
method get_user_pref (line 544) | async def get_user_pref(self, user_id: int, key: str, default: Any = N...
method set_user_pref (line 557) | async def set_user_pref(self, user_id: int, key: str, value: Any):
method get_total_stats (line 572) | async def get_total_stats(self) -> Tuple[int, int]:
method get_all_user_ids (line 577) | async def get_all_user_ids(self) -> List[int]:
method is_maintenance (line 581) | async def is_maintenance(self) -> bool:
method set_maintenance (line 586) | async def set_maintenance(self, state: bool):
function escape (line 595) | def escape(s: str) -> str:
function is_valid_srt (line 598) | def is_valid_srt(content: str) -> bool:
function vtt_to_srt (line 612) | def vtt_to_srt(content: str) -> str:
function clean_filename (line 641) | def clean_filename(text: str) -> str:
function make_filename (line 644) | def make_filename(movie: str, ext: str = ".srt") -> str:
function clean_language_name (line 647) | def clean_language_name(lang: str) -> str:
function is_movie_query (line 651) | def is_movie_query(text: str) -> bool:
function is_clean_query (line 679) | def is_clean_query(t: str) -> bool:
function _score_title (line 682) | def _score_title(result_title: str, query: str) -> int:
function rank_results (line 706) | def rank_results(results: List[dict], query: str) -> List[dict]:
function deduplicate (line 715) | def deduplicate(results: List[dict]) -> List[dict]:
function http_get (line 731) | def http_get(
function _scrape_get (line 757) | def _scrape_get(url: str, params: dict = None, timeout: int = 15) -> Opt...
function extract_zip_srt (line 775) | def extract_zip_srt(zip_path: str) -> Optional[str]:
function download_file (line 792) | def download_file(url: str, depth: int = 0) -> Tuple[Optional[str], Opti...
function read_file_content (line 839) | def read_file_content(path: str) -> Optional[str]:
function send_subtitle_file (line 848) | async def send_subtitle_file(
function _retry_source (line 874) | async def _retry_source(func: Callable, q: str, retries: int = 2) -> Lis...
function search_wyzie (line 889) | def search_wyzie(q: str) -> List[dict]:
function search_goat (line 918) | def search_goat(q: str) -> List[dict]:
function search_malsub_org (line 944) | def search_malsub_org(q: str) -> List[dict]:
function search_jerry (line 967) | def search_jerry(q: str) -> List[dict]:
function get_jerry_langs (line 981) | def get_jerry_langs(url: str) -> List[dict]:
function search_ironman (line 995) | def search_ironman(q: str) -> List[dict]:
function get_ironman_langs (line 1007) | def get_ironman_langs(url: str) -> List[dict]:
function _deep_block (line 1022) | def _deep_block(text: str, lang: str) -> str:
function _gtx_batch (line 1033) | async def _gtx_batch(
function translate_srt_async (line 1064) | async def translate_srt_async(
function _try_send (line 1130) | async def _try_send(bot, chat_id: int, text: str) -> bool:
class BotHandlers (line 1141) | class BotHandlers:
method __init__ (line 1142) | def __init__(self, db: Database):
method _is_limited (line 1155) | async def _is_limited(self, user_id: int) -> bool:
method _wait_sec (line 1164) | async def _wait_sec(self, user_id: int) -> int:
method _check_membership (line 1171) | async def _check_membership(self, bot, user_id: int, force: bool = Fal...
method _cache_set (line 1191) | async def _cache_set(self, key: str, value: Any):
method gate_user (line 1199) | async def gate_user(self, update: Update, ctx: ContextTypes.DEFAULT_TY...
method _search_all_live (line 1226) | async def _search_all_live(self, q: str, status_msg: Message) -> Tuple:
method _run_search (line 1266) | async def _run_search(
method _run_translation (line 1366) | async def _run_translation(
method cmd_start (line 1422) | async def cmd_start(self, update: Update, ctx: ContextTypes.DEFAULT_TY...
method cmd_help (line 1439) | async def cmd_help(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
method cmd_cancel (line 1471) | async def cmd_cancel(self, update: Update, ctx: ContextTypes.DEFAULT_T...
method cmd_search (line 1480) | async def cmd_search(self, update: Update, ctx: ContextTypes.DEFAULT_T...
method cmd_translate (line 1507) | async def cmd_translate(self, update: Update, ctx: ContextTypes.DEFAUL...
method cmd_history (line 1539) | async def cmd_history(self, update: Update, ctx: ContextTypes.DEFAULT_...
method cmd_mystats (line 1561) | async def cmd_mystats(self, update: Update, ctx: ContextTypes.DEFAULT_...
method cmd_leaderboard (line 1585) | async def cmd_leaderboard(self, update: Update, ctx: ContextTypes.DEFA...
method cmd_trending (line 1608) | async def cmd_trending(self, update: Update, ctx: ContextTypes.DEFAULT...
method cmd_request (line 1628) | async def cmd_request(self, update: Update, ctx: ContextTypes.DEFAULT_...
method cmd_donate (line 1670) | async def cmd_donate(self, update: Update, ctx: ContextTypes.DEFAULT_T...
method cmd_group (line 1683) | async def cmd_group(self, update: Update, ctx: ContextTypes.DEFAULT_TY...
method cmd_report (line 1691) | async def cmd_report(self, update: Update, ctx: ContextTypes.DEFAULT_T...
method cmd_feedback (line 1719) | async def cmd_feedback(self, update: Update, ctx: ContextTypes.DEFAULT...
method cmd_prefs (line 1742) | async def cmd_prefs(self, update: Update, ctx: ContextTypes.DEFAULT_TY...
method cmd_admin (line 1781) | async def cmd_admin(self, update: Update, ctx: ContextTypes.DEFAULT_TY...
method cmd_stats (line 1801) | async def cmd_stats(self, update: Update, ctx: ContextTypes.DEFAULT_TY...
method cmd_ping (line 1822) | async def cmd_ping(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
method cmd_broadcast (line 1833) | async def cmd_broadcast(self, update: Update, ctx: ContextTypes.DEFAUL...
method cmd_ban (line 1864) | async def cmd_ban(self, update: Update, ctx: ContextTypes.DEFAULT_TYPE):
method cmd_unban (line 1880) | async def cmd_unban(self, update: Update, ctx: ContextTypes.DEFAULT_TY...
method cmd_banned (line 1896) | async def cmd_banned(self, update: Update, ctx: ContextTypes.DEFAULT_T...
method cmd_requests (line 1915) | async def cmd_requests(self, update: Update, ctx: ContextTypes.DEFAULT...
method cmd_donereq (line 1934) | async def cmd_donereq(self, update: Update, ctx: ContextTypes.DEFAULT_...
method cmd_userinfo (line 1947) | async def cmd_userinfo(self, update: Update, ctx: ContextTypes.DEFAULT...
method cmd_maintenance (line 1979) | async def cmd_maintenance(self, update: Update, ctx: ContextTypes.DEFA...
method cmd_clearcache (line 1992) | async def cmd_clearcache(self, update: Update, ctx: ContextTypes.DEFAU...
method handle_message (line 2005) | async def handle_message(self, update: Update, ctx: ContextTypes.DEFAU...
method inline_query (line 2044) | async def inline_query(self, update: Update, ctx: ContextTypes.DEFAULT...
method handle_callback (line 2094) | async def handle_callback(self, update: Update, ctx: ContextTypes.DEFA...
class HealthHandler (line 2392) | class HealthHandler(BaseHTTPRequestHandler):
method do_GET (line 2393) | def do_GET(self):
method log_message (line 2400) | def log_message(self, *a, **kw):
function run_health (line 2403) | def run_health(port: int):
function post_init (line 2409) | async def post_init(app: Application):
function post_stop (line 2421) | async def post_stop(app: Application):
function main (line 2428) | def main():
Condensed preview — 4 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (113K chars).
[
{
"path": ".gitignore",
"chars": 202,
"preview": "# Python\n__pycache__/\n*.py[cod]\n*.so\n*.egg-info/\ndist/\nbuild/\n\n# Virtual environment\nvenv/\nenv/\n.env\n\n# Bot data & logs\n"
},
{
"path": "README.md",
"chars": 1025,
"preview": "# MunaX Subtitle Engine – True Final Boss\n\nA lightning‑fast Telegram bot that searches 5 subtitle sources, delivers Mala"
},
{
"path": "bot.py",
"chars": 105718,
"preview": "#!/usr/bin/env python3\n\"\"\"\n╔══════════════════════════════════════════════════════════════╗\n║ 𝗠𝘂𝗻𝗮𝗫 𝗦𝘂𝗯𝘀 ☠️⚡ UL"
},
{
"path": "requirements.txt",
"chars": 144,
"preview": "python-telegram-bot[webhooks]>=20.7\nrequests>=2.31.0\nhttpx>=0.25.0\nbeautifulsoup4>=4.12.0\ndeep-translator>=1.11.0\nlxml>="
}
]
About this extraction
This page contains the full source code of the shamilmyran/sub-telegram GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 4 files (104.6 KB), approximately 26.2k tokens, and a symbol index with 124 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.