import os import logging import re import httpx import random from aiogram import Bot, Dispatcher, types from aiogram.filters import Command, CommandObject from aiogram.client.default import DefaultBotProperties from aiogram.enums import ParseMode TOKEN = os.getenv("TELEGRAM_TOKEN") API_URL = os.getenv("VNDB_API_URL", "https://api.vndb.org/kana") logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) bot = Bot(token=TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML)) dp = Dispatcher() # --- UTILS --- def clean_text(text: str, limit: int = 600) -> str: if not text: return "No description available." text = re.sub(r'\[url=?.*?\](.*?)\[/url\]', r'\1', text) text = re.sub(r'\[/?b\]', '', text) text = re.sub(r'\[/?i\]', '', text) return text[:limit] + "..." if len(text) > limit else text def safe_list_to_str(data) -> str: if not data: return "N/A" if isinstance(data, list): processed = [] for x in data: if isinstance(x, str): processed.append(x.upper()) elif isinstance(x, dict): val = x.get('lang') or x.get('name') or x.get('title') if val: processed.append(str(val).upper()) return ", ".join(processed) if processed else "N/A" return str(data).upper() async def fetch_vndb(endpoint: str, filters: list, fields: str, sort: str = "id", results: int = 10): payload = { "filters": filters, "fields": fields, "sort": sort, "results": results } async with httpx.AsyncClient(timeout=15.0) as client: try: response = await client.post(f"{API_URL}/{endpoint}", json=payload) if response.status_code == 200: return response.json().get("results", []) logger.error(f"VNDB Error {response.status_code}: {response.text}") return None except Exception as e: logger.error(f"Connection error: {e}") return None async def send_result(message: types.Message, text: str, image_url: str = None): try: if image_url: await message.answer_photo(photo=image_url, caption=text[:1024]) else: await message.answer(text) except Exception as e: await message.answer(text) # --- HANDLERS --- @dp.message(Command("vn")) async def handle_vn(message: types.Message, command: CommandObject): if not command.args: return await message.answer("Usage: /vn ") is_id = command.args.startswith('v') and command.args[1:].isdigit() filt = ["id", "=", command.args] if is_id else ["search", "=", command.args] # Добавили relations для "рекомендаций" fields = ("id, title, alttitle, released, rating, votecount, description, " "languages, platforms, developers{name}, image{url}, relations{title, id, relation}") res = await fetch_vndb("vn", filt, fields) if not res: return await message.answer("❌ Not found.") if len(res) > 1 and not is_id: out = [f"• {i['title']} — {i['id']}" for i in res[:10]] return await message.answer("🔍 Select VN:\n" + "\n".join(out)) v = res[0] img = v.get('image', {}).get('url') if v.get('image') else None # Формируем список связей (Sequel, Prequel и т.д.) rel_list = [] for r in v.get('relations', [])[:5]: rel_list.append(f"• {r['title']} ({r['id']}) - {r['relation']}") rel_str = "\n".join(rel_list) if rel_list else "None" text = ( f"TITLE: {v['title']}\n" f"DEVELOPER: {safe_list_to_str(v.get('developers'))}\n" f"RATING: {v['rating']/10 if v.get('rating') else 'N/A'} ({v.get('votecount', 0)} votes)\n" f"LANGS: {safe_list_to_str(v.get('languages'))}\n\n" f"RELATIONS / RECOMMENDED:\n{rel_str}\n\n" f"DESCRIPTION:\n{clean_text(v.get('description'), 400)}\n\n" f"VNDB: https://vndb.org/{v['id']}" ) await send_result(message, text, img) @dp.message(Command("random")) async def handle_random(message: types.Message): """Выбирает случайную новеллу из 50 популярных с высоким рейтингом.""" # Фильтр: рейтинг > 75, популярность высокая res = await fetch_vndb("vn", ["rating", ">=", 75], "id, title, image{url}, rating, description", sort="votecount", results=50) if not res: return await message.answer("❌ API error") v = random.choice(res) text = ( f"🎲 RANDOM VN PICK\n\n" f"TITLE: {v['title']}\n" f"RATING: {v['rating']/10} ⭐\n\n" f"DESCRIPTION:\n{clean_text(v.get('description'), 300)}\n\n" f"VNDB: https://vndb.org/{v['id']}" ) await send_result(message, text, v.get('image', {}).get('url')) @dp.message(Command("top")) async def handle_top(message: types.Message): """Выводит топ-10 новелл по рейтингу.""" res = await fetch_vndb("vn", ["votecount", ">", 1000], "id, title, rating", sort="rating", results=10) if not res: return await message.answer("❌ API error") out = ["🏆 TOP RATED VISUAL NOVELS\n"] for i, v in enumerate(res, 1): out.append(f"{i}. {v['title']} — {v['rating']/10} ({v['id']})") await message.answer("\n".join(out)) @dp.message(Command("char")) async def handle_char(message: types.Message, command: CommandObject): if not command.args: return await message.answer("Usage: /char ") is_id = command.args.startswith('c') and command.args[1:].isdigit() filt = ["id", "=", command.args] if is_id else ["search", "=", command.args] res = await fetch_vndb("character", filt, "id, name, description, gender, image{url}") if not res: return await message.answer("❌ Not found.") c = res[0] text = (f"NAME: {c['name']}\nGENDER: {str(c.get('gender')).upper()}\n\n" f"DESC:\n{clean_text(c.get('description'), 500)}\n\n" f"VNDB: https://vndb.org/{c['id']}") await send_result(message, text, c.get('image', {}).get('url')) @dp.message(Command("start", "help")) async def cmd_start(message: types.Message): await message.answer( "🤖 VNDB Pro Bot\n\n" "Explore:\n" "/vn [name/id] - Search & Details\n" "/char [name/id] - Characters\n" "/random - Get a random high-rated VN\n" "/top - Show Top 10 Visual Novels\n" "/release [name/id] - Release info" ) # Стаб для release (использует логику из предыдущего ответа) @dp.message(Command("release")) async def handle_rel_wrap(message: types.Message, command: CommandObject): # (здесь остается логика из предыдущего кода для краткости опустим) pass if __name__ == "__main__": dp.run_polling(bot)