""" بوت تيليغرام للمانغا العربية يدعم كل المواقع العربية الموجودة في keiyoushi/extensions-source """ import os import re import asyncio import logging from typing import Optional import requests from bs4 import BeautifulSoup from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup from telegram.ext import ApplicationBuilder, CommandHandler, MessageHandler, \ CallbackQueryHandler, ContextTypes, ConversationHandler, filters # ══════════════════════════════════════════════ # الإعدادات # ══════════════════════════════════════════════ BOT_TOKEN = os.environ.get("BOT_TOKEN", "ضع_التوكن_هنا") HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"} logging.basicConfig(level=logging.INFO) log = logging.getLogger(__name__) # ══════════════════════════════════════════════ # قاعدة بيانات المواقع # كل موقع عنده scraper خاص # ══════════════════════════════════════════════ SITES = { # ── Madara (WordPress) ── نفس النظام لأغلب المواقع "mangalek": {"name": "مانجا ليك", "url": "https://lek-manga.net", "type": "madara"}, "mangastarz": {"name": "مانجا ستارز", "url": "https://manga-starz.net", "type": "madara"}, "mangaspark": {"name": "مانجا سبارك", "url": "https://mangaspark.me", "type": "madara"}, "mangalink": {"name": "مانجا لينك", "url": "https://manga-link.org", "type": "madara"}, "mangalionz": {"name": "مانجا ليونز", "url": "https://mangalionz.com", "type": "madara"}, # ── مواقع بنظام مختلف "onma": {"name": "أونما", "url": "https://onma.me", "type": "onma"}, "olympus": {"name": "أولمبس", "url": "https://olympustaff.com", "type": "olympus"}, } # ══════════════════════════════════════════════ # Scrapers — كل نوع موقع له scraper # ══════════════════════════════════════════════ def _get(url: str, **kwargs) -> Optional[BeautifulSoup]: try: r = requests.get(url, headers=HEADERS, timeout=15, **kwargs) r.raise_for_status() return BeautifulSoup(r.text, "html.parser") except Exception as e: log.error(f"GET {url} → {e}") return None def _post(url: str, data: dict) -> Optional[BeautifulSoup]: try: r = requests.post(url, headers=HEADERS, data=data, timeout=15) r.raise_for_status() return BeautifulSoup(r.text, "html.parser") except Exception as e: log.error(f"POST {url} → {e}") return None # ── نظام Madara (WordPress) ────────────────── class MadaraScraper: def __init__(self, base_url: str): self.base = base_url.rstrip("/") def search(self, query: str) -> list[dict]: """البحث عن مانغا""" soup = _get(f"{self.base}/?s={query}&post_type=wp-manga") if not soup: return [] results = [] for item in soup.select(".c-tabs-item__content, .post-title"): a = item.select_one("a") if a: results.append({ "title": a.get_text(strip=True), "url": a["href"], "slug": a["href"].rstrip("/").split("/")[-1], }) return results[:8] def get_chapters(self, manga_url: str) -> list[dict]: """جلب قائمة الفصول""" soup = _get(manga_url) if not soup: return [] # استخرج manga_id للـ AJAX manga_id = None for el in soup.select("[id^='manga-chapters-holder']"): manga_id = el.get("data-id") break chapters = [] if manga_id: # طريقة AJAX (أحدث) ajax_soup = _post( f"{self.base}/wp-admin/admin-ajax.php", {"action": "manga_get_chapters", "manga": manga_id} ) if ajax_soup: soup = ajax_soup for li in soup.select("li.wp-manga-chapter a, .chapter-li a"): href = li.get("href", "") if not href: continue num_match = re.search(r"chapter[- _](\d+\.?\d*)", href, re.I) num = num_match.group(1) if num_match else li.get_text(strip=True) chapters.append({ "name": li.get_text(strip=True) or f"فصل {num}", "url": href, "num": num, }) return chapters def get_images(self, chapter_url: str) -> list[str]: """جلب صور الفصل""" soup = _get(chapter_url) if not soup: return [] imgs = soup.select(".page-break img, .reading-content img, img.wp-manga-chapter-img") return [ (img.get("data-src") or img.get("data-lazy-src") or img.get("src", "")).strip() for img in imgs if img.get("data-src") or img.get("data-lazy-src") or img.get("src") ] # ── Olympus Staff ──────────────────────────── class OlympusScraper: def __init__(self): self.base = "https://olympustaff.com" def search(self, query: str) -> list[dict]: soup = _get(f"{self.base}/series?search={query}") if not soup: return [] results = [] for a in soup.select("a[href*='/series/'][title]"): slug = a["href"].rstrip("/").split("/")[-1] results.append({ "title": a.get("title", slug), "url": a["href"], "slug": slug, }) return results[:8] def get_chapters(self, manga_url: str) -> list[dict]: soup = _get(manga_url) if not soup: return [] chapters = [] for a in soup.select("a[href*='/series/']"): href = a.get("href", "") parts = href.rstrip("/").split("/") if len(parts) >= 2: last = parts[-1] if re.match(r"^\d+\.?\d*$", last): chapters.append({ "name": f"فصل {last}", "url": href, "num": last, }) # إزالة التكرار seen = set() unique = [] for c in chapters: if c["url"] not in seen: seen.add(c["url"]) unique.append(c) return unique def get_images(self, chapter_url: str) -> list[str]: soup = _get(chapter_url) if not soup: return [] return [ img["src"] for img in soup.select("img[src*='/uploads/']") ] # ── Onma ───────────────────────────────────── class OnmaScraper: def __init__(self): self.base = "https://onma.me" def search(self, query: str) -> list[dict]: soup = _get(f"{self.base}/?s={query}") if not soup: return [] results = [] for a in soup.select("h3.post-title a, .manga_title a"): results.append({ "title": a.get_text(strip=True), "url": a["href"], "slug": a["href"].rstrip("/").split("/")[-1], }) return results[:8] def get_chapters(self, manga_url: str) -> list[dict]: # onma يستخدم Madara أيضاً return MadaraScraper(self.base).get_chapters(manga_url) def get_images(self, chapter_url: str) -> list[str]: return MadaraScraper(self.base).get_images(chapter_url) # ── Factory: يرجع الـ scraper المناسب ──────── def get_scraper(site_key: str): site = SITES.get(site_key) if not site: return None t = site["type"] if t == "madara": return MadaraScraper(site["url"]) elif t == "olympus": return OlympusScraper() elif t == "onma": return OnmaScraper() return None # ══════════════════════════════════════════════ # حالات المحادثة # ══════════════════════════════════════════════ SELECT_SITE, SELECT_MANGA, SELECT_CHAPTER = range(3) # ══════════════════════════════════════════════ # معالجات البوت # ══════════════════════════════════════════════ async def cmd_start(update: Update, ctx: ContextTypes.DEFAULT_TYPE): await update.message.reply_text( "🎌 *مرحباً بك في بوت المانغا العربية!*\n\n" "الأوامر:\n" "🔍 /search — ابحث عن مانغا\n" "📋 /sites — قائمة المواقع المدعومة\n" "❓ /help — المساعدة", parse_mode="Markdown" ) async def cmd_sites(update: Update, ctx: ContextTypes.DEFAULT_TYPE): text = "📋 *المواقع المدعومة:*\n\n" for key, site in SITES.items(): text += f"• `{key}` — {site['name']} ({site['url']})\n" await update.message.reply_text(text, parse_mode="Markdown") async def cmd_search(update: Update, ctx: ContextTypes.DEFAULT_TYPE): """الخطوة 1: اختيار الموقع""" buttons = [ [InlineKeyboardButton(f"{s['name']}", callback_data=f"site:{k}")] for k, s in SITES.items() ] await update.message.reply_text( "🌐 *اختر الموقع:*", reply_markup=InlineKeyboardMarkup(buttons), parse_mode="Markdown" ) return SELECT_SITE async def cb_select_site(update: Update, ctx: ContextTypes.DEFAULT_TYPE): """الخطوة 1 → اختار الموقع""" query = update.callback_query await query.answer() site_key = query.data.split(":")[1] ctx.user_data["site"] = site_key site_name = SITES[site_key]["name"] await query.edit_message_text( f"✅ اخترت: *{site_name}*\n\n" "🔍 اكتب اسم المانغا للبحث:", parse_mode="Markdown" ) return SELECT_MANGA async def handle_search_query(update: Update, ctx: ContextTypes.DEFAULT_TYPE): """الخطوة 2: البحث وعرض النتائج""" site_key = ctx.user_data.get("site") if not site_key: await update.message.reply_text("❌ اكتب /search أول") return ConversationHandler.END query = update.message.text scraper = get_scraper(site_key) if not scraper: await update.message.reply_text("❌ موقع غير مدعوم") return ConversationHandler.END msg = await update.message.reply_text("⏳ جاري البحث...") results = scraper.search(query) if not results: await msg.edit_text("❌ ما لقيت نتائج، جرب اسم ثاني") return ConversationHandler.END # خزّن النتائج في الذاكرة ctx.user_data["results"] = results buttons = [ [InlineKeyboardButton(r["title"][:50], callback_data=f"manga:{i}")] for i, r in enumerate(results) ] await msg.edit_text( f"📚 النتائج ({len(results)}):", reply_markup=InlineKeyboardMarkup(buttons) ) return SELECT_CHAPTER async def cb_select_manga(update: Update, ctx: ContextTypes.DEFAULT_TYPE): """الخطوة 3: اختيار مانغا وعرض الفصول""" query = update.callback_query await query.answer() idx = int(query.data.split(":")[1]) results = ctx.user_data.get("results", []) site_key = ctx.user_data.get("site") if idx >= len(results): await query.edit_message_text("❌ خطأ") return ConversationHandler.END manga = results[idx] ctx.user_data["manga"] = manga await query.edit_message_text(f"⏳ جاري جلب فصول: *{manga['title']}*...", parse_mode="Markdown") scraper = get_scraper(site_key) chapters = scraper.get_chapters(manga["url"]) if not chapters: await query.edit_message_text("❌ ما لقيت فصول، الموقع قد يحتاج تسجيل دخول") return ConversationHandler.END ctx.user_data["chapters"] = chapters # عرض أول 20 فصل (الأحدث) show = chapters[:20] btns = [ [InlineKeyboardButton(c["name"][:50], callback_data=f"chap:{i}")] for i, c in enumerate(show) ] btns += [[InlineKeyboardButton("📋 المزيد", callback_data="chap:more")]] await query.edit_message_text( f"📖 *{manga['title']}*\n{len(chapters)} فصل — اختر فصل:", reply_markup=InlineKeyboardMarkup(btns), parse_mode="Markdown" ) return SELECT_CHAPTER async def cb_select_chapter(update: Update, ctx: ContextTypes.DEFAULT_TYPE): """الخطوة 4: إرسال صور الفصل""" query = update.callback_query await query.answer() idx = query.data.split(":")[1] if idx == "more": # عرض باقي الفصول chapters = ctx.user_data.get("chapters", []) show = chapters[20:40] btns = [ [InlineKeyboardButton(c["name"][:50], callback_data=f"chap:{i+20}")] for i, c in enumerate(show) ] await query.edit_message_text( "📋 المزيد من الفصول:", reply_markup=InlineKeyboardMarkup(btns) ) return SELECT_CHAPTER idx = int(idx) chapters = ctx.user_data.get("chapters", []) site_key = ctx.user_data.get("site") chapter = chapters[idx] await query.edit_message_text(f"⏳ جاري تحميل: *{chapter['name']}*...", parse_mode="Markdown") scraper = get_scraper(site_key) images = scraper.get_images(chapter["url"]) if not images: await query.edit_message_text( "❌ ما أقدر أجيب الصور\n\n" "ممكن الفصل مدفوع أو الموقع محجوب\n" f"🔗 [افتح مباشرة]({chapter['url']})", parse_mode="Markdown" ) return ConversationHandler.END manga_title = ctx.user_data.get("manga", {}).get("title", "") await query.edit_message_text( f"📤 جاري إرسال *{chapter['name']}*\n" f"({len(images)} صورة)...", parse_mode="Markdown" ) # إرسال الصور — كل 10 كـ album chat_id = update.effective_chat.id sent = 0 for i in range(0, min(len(images), 50), 10): batch = images[i:i+10] try: from telegram import InputMediaPhoto media = [InputMediaPhoto(url) for url in batch] await ctx.bot.send_media_group(chat_id, media) sent += len(batch) except Exception as e: log.warning(f"فشل إرسال album: {e}") # حاول إرسال صورة صورة for url in batch: try: await ctx.bot.send_photo(chat_id, url) sent += 1 except: pass await ctx.bot.send_message( chat_id, f"✅ تم إرسال {sent}/{len(images)} صورة\n" f"📖 {manga_title} — {chapter['name']}\n" f"🔗 [المصدر]({chapter['url']})", parse_mode="Markdown" ) return ConversationHandler.END async def cmd_cancel(update: Update, ctx: ContextTypes.DEFAULT_TYPE): await update.message.reply_text("❌ تم الإلغاء") return ConversationHandler.END # ── أمر مختصر: /get olympus demonic-emperor 837 ── async def cmd_get(update: Update, ctx: ContextTypes.DEFAULT_TYPE): args = ctx.args if len(args) < 3: await update.message.reply_text( "❌ الاستخدام:\n" "`/get `\n\n" "مثال:\n" "`/get olympus demonic-emperor 837`", parse_mode="Markdown" ) return site_key, slug, chapter_num = args[0], args[1], args[2] site = SITES.get(site_key) if not site: await update.message.reply_text(f"❌ موقع غير معروف: `{site_key}`\n\nاستخدم /sites لرؤية القائمة", parse_mode="Markdown") return # بناء رابط الفصل if site_key == "olympus": chapter_url = f"https://olympustaff.com/series/{slug}/{chapter_num}" elif site["type"] == "madara": chapter_url = f"{site['url']}/manga/{slug}/chapter-{chapter_num}" else: chapter_url = f"{site['url']}/manga/{slug}/chapter-{chapter_num}" msg = await update.message.reply_text(f"⏳ جاري جلب الفصل {chapter_num} من {site['name']}...") scraper = get_scraper(site_key) images = scraper.get_images(chapter_url) if not images: await msg.edit_text( f"❌ ما أقدر أجيب الصور\n" f"🔗 [افتح مباشرة]({chapter_url})", parse_mode="Markdown" ) return await msg.edit_text(f"📤 إرسال {len(images)} صورة...") chat_id = update.effective_chat.id sent = 0 for i in range(0, min(len(images), 50), 10): batch = images[i:i+10] try: from telegram import InputMediaPhoto await ctx.bot.send_media_group(chat_id, [InputMediaPhoto(u) for u in batch]) sent += len(batch) except Exception as e: for url in batch: try: await ctx.bot.send_photo(chat_id, url) sent += 1 except: pass await ctx.bot.send_message( chat_id, f"✅ تم! {sent}/{len(images)} صورة\n" f"🔗 [المصدر]({chapter_url})", parse_mode="Markdown" ) # ══════════════════════════════════════════════ # تشغيل البوت # ══════════════════════════════════════════════ def build_app(): """بناء التطبيق فقط — بدون تشغيل""" application = ApplicationBuilder().token(BOT_TOKEN).build() conv = ConversationHandler( entry_points=[CommandHandler("search", cmd_search)], states={ SELECT_SITE: [CallbackQueryHandler(cb_select_site, pattern=r"^site:")], SELECT_MANGA: [ MessageHandler(filters.TEXT & ~filters.COMMAND, handle_search_query), ], SELECT_CHAPTER: [ CallbackQueryHandler(cb_select_manga, pattern=r"^manga:"), CallbackQueryHandler(cb_select_chapter, pattern=r"^chap:"), ], }, fallbacks=[CommandHandler("cancel", cmd_cancel)], per_message=False, ) application.add_handler(CommandHandler("start", cmd_start)) application.add_handler(CommandHandler("sites", cmd_sites)) application.add_handler(CommandHandler("get", cmd_get)) application.add_handler(conv) return application async def run_async(): """تشغيل البوت بدون signal handlers — يشتغل من أي thread""" application = build_app() await application.initialize() await application.start() await application.updater.start_polling( drop_pending_updates=True, allowed_updates=Update.ALL_TYPES, ) print("✅ البوت شغال!") # انتظر إلى الأبد await asyncio.Event().wait() def main(): asyncio.run(run_async()) if __name__ == "__main__": main()