| """ |
| ุจูุช ุชูููุบุฑุงู
ููู
ุงูุบุง ุงูุนุฑุจูุฉ |
| ูุฏุนู
ูู ุงูู
ูุงูุน ุงูุนุฑุจูุฉ ุงูู
ูุฌูุฏุฉ ูู 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__) |
|
|
| |
| |
| |
| |
|
|
| SITES = { |
| |
| "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"}, |
| } |
|
|
| |
| |
| |
|
|
| 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 |
|
|
| |
| 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 = None |
| for el in soup.select("[id^='manga-chapters-holder']"): |
| manga_id = el.get("data-id") |
| break |
|
|
| chapters = [] |
|
|
| if manga_id: |
| |
| 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") |
| ] |
|
|
| |
| 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/']") |
| ] |
|
|
| |
| 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]: |
| |
| 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) |
|
|
| |
| 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 |
|
|
| |
| 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" |
| ) |
|
|
| |
| 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 |
|
|
| |
| async def cmd_get(update: Update, ctx: ContextTypes.DEFAULT_TYPE): |
| args = ctx.args |
| if len(args) < 3: |
| await update.message.reply_text( |
| "โ ุงูุงุณุชุฎุฏุงู
:\n" |
| "`/get <site> <slug> <chapter>`\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() |