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