Compare commits

..

2 Commits

167
bot.py
View File

@@ -3,6 +3,7 @@ import logging
import re import re
import httpx import httpx
import random import random
import random
from aiogram import Bot, Dispatcher, types from aiogram import Bot, Dispatcher, types
from aiogram.filters import Command, CommandObject from aiogram.filters import Command, CommandObject
from aiogram.client.default import DefaultBotProperties from aiogram.client.default import DefaultBotProperties
@@ -19,12 +20,14 @@ dp = Dispatcher()
# --- UTILS --- # --- UTILS ---
def clean_text(text: str, limit: int = 600) -> str: def clean_text(text: str, limit: int = 900) -> str:
if not text: return "No description available." if not text: return "No description available."
text = re.sub(r'\[url=?.*?\](.*?)\[/url\]', r'\1', text) # Удаляем все теги [b], [i], [url] и т.д.
text = re.sub(r'\[/?b\]', '', text) text = re.sub(r'\[.*?\]', '', text)
text = re.sub(r'\[/?i\]', '', text) text = text.replace('"', "'")
return text[:limit] + "..." if len(text) > limit else text if len(text) > limit:
return text[:limit].rsplit(' ', 1)[0] + "..."
return text
def safe_list_to_str(data) -> str: def safe_list_to_str(data) -> str:
if not data: return "N/A" if not data: return "N/A"
@@ -33,18 +36,14 @@ def safe_list_to_str(data) -> str:
for x in data: for x in data:
if isinstance(x, str): processed.append(x.upper()) if isinstance(x, str): processed.append(x.upper())
elif isinstance(x, dict): elif isinstance(x, dict):
val = x.get('lang') or x.get('name') or x.get('title') # Ищем любое человекочитаемое поле
val = x.get('lang') or x.get('name') or x.get('title') or x.get('label')
if val: processed.append(str(val).upper()) if val: processed.append(str(val).upper())
return ", ".join(processed) if processed else "N/A" return ", ".join(processed) if processed else "N/A"
return str(data).upper() return str(data).upper()
async def fetch_vndb(endpoint: str, filters: list, fields: str, sort: str = "id", results: int = 10): async def fetch_vndb(endpoint: str, filters: list, fields: str, sort: str = "id", results: int = 10):
payload = { payload = {"filters": filters, "fields": fields, "sort": sort, "results": results}
"filters": filters,
"fields": fields,
"sort": sort,
"results": results
}
async with httpx.AsyncClient(timeout=15.0) as client: async with httpx.AsyncClient(timeout=15.0) as client:
try: try:
response = await client.post(f"{API_URL}/{endpoint}", json=payload) response = await client.post(f"{API_URL}/{endpoint}", json=payload)
@@ -53,7 +52,7 @@ async def fetch_vndb(endpoint: str, filters: list, fields: str, sort: str = "id"
logger.error(f"VNDB Error {response.status_code}: {response.text}") logger.error(f"VNDB Error {response.status_code}: {response.text}")
return None return None
except Exception as e: except Exception as e:
logger.error(f"Connection error: {e}") logger.error(f"Error: {e}")
return None return None
async def send_result(message: types.Message, text: str, image_url: str = None): async def send_result(message: types.Message, text: str, image_url: str = None):
@@ -65,6 +64,13 @@ async def send_result(message: types.Message, text: str, image_url: str = None):
except Exception as e: except Exception as e:
await message.answer(text) await message.answer(text)
def find_exact_match(results, query, attr):
"""Ищет 100% совпадение по имени/названию в результатах."""
for item in results:
if item.get(attr, "").lower() == query.lower():
return item
return None
# --- HANDLERS --- # --- HANDLERS ---
@dp.message(Command("vn")) @dp.message(Command("vn"))
@@ -73,34 +79,34 @@ async def handle_vn(message: types.Message, command: CommandObject):
is_id = command.args.startswith('v') and command.args[1:].isdigit() is_id = command.args.startswith('v') and command.args[1:].isdigit()
filt = ["id", "=", command.args] if is_id else ["search", "=", command.args] filt = ["id", "=", command.args] if is_id else ["search", "=", command.args]
fields = "id, title, alttitle, released, rating, votecount, description, languages, platforms, developers{name}, image{url}, relations{title, id, relation}"
# Добавили 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) res = await fetch_vndb("vn", filt, fields)
if not res: return await message.answer("❌ Not found.") if not res: return await message.answer("❌ Not found.")
if len(res) > 1 and not is_id:
# Логика умного поиска
target = res[0]
if not is_id and len(res) > 1:
exact = find_exact_match(res, command.args, 'title')
if exact:
target = exact
else:
out = [f"{i['title']} — <code>{i['id']}</code>" for i in res[:10]] out = [f"{i['title']} — <code>{i['id']}</code>" for i in res[:10]]
return await message.answer("🔍 <b>Select VN:</b>\n" + "\n".join(out)) return await message.answer("🔍 <b>Select VN:</b>\n" + "\n".join(out))
v = res[0] img = target.get('image', {}).get('url') if target.get('image') else None
img = v.get('image', {}).get('url') if v.get('image') else None rel_str = "\n".join([f"{r['title']} (<code>{r['id']}</code>) - {r['relation']}" for r in target.get('relations', [])[:3]])
# Формируем список связей (Sequel, Prequel и т.д.)
rel_list = []
for r in v.get('relations', [])[:5]:
rel_list.append(f"{r['title']} (<code>{r['id']}</code>) - <i>{r['relation']}</i>")
rel_str = "\n".join(rel_list) if rel_list else "None"
text = ( text = (
f"<b>TITLE:</b> {v['title']}\n" f"<b>TITLE:</b> {target['title']}\n"
f"<b>DEVELOPER:</b> {safe_list_to_str(v.get('developers'))}\n" f"<b>ORIGINAL:</b> {target.get('alttitle') or 'N/A'}\n"
f"<b>RATING:</b> {v['rating']/10 if v.get('rating') else 'N/A'} ({v.get('votecount', 0)} votes)\n" f"<b>DEVELOPER:</b> {safe_list_to_str(target.get('developers'))}\n"
f"<b>LANGS:</b> {safe_list_to_str(v.get('languages'))}\n\n" f"<b>RATING:</b> {target['rating']/10 if target.get('rating') else 'N/A'} ({target.get('votecount', 0)} votes)\n"
f"<b>RELATIONS / RECOMMENDED:</b>\n{rel_str}\n\n" f"<b>LANGS:</b> {safe_list_to_str(target.get('languages'))}\n"
f"<b>DESCRIPTION:</b>\n<i>{clean_text(v.get('description'), 400)}</i>\n\n" f"<b>PLATFORMS:</b> {safe_list_to_str(target.get('platforms'))}\n\n"
f"<b>VNDB:</b> https://vndb.org/{v['id']}" f"<b>RELATIONS:</b>\n{rel_str or 'None'}\n\n"
f"<b>DESC:</b>\n<i>{clean_text(target.get('description'), 450)}</i>\n\n"
f"<b>VNDB:</b> https://vndb.org/{target['id']}"
) )
await send_result(message, text, img) await send_result(message, text, img)
@@ -138,31 +144,86 @@ async def handle_char(message: types.Message, command: CommandObject):
if not command.args: return await message.answer("Usage: /char <name/id>") if not command.args: return await message.answer("Usage: /char <name/id>")
is_id = command.args.startswith('c') and command.args[1:].isdigit() is_id = command.args.startswith('c') and command.args[1:].isdigit()
filt = ["id", "=", command.args] if is_id else ["search", "=", command.args] filt = ["id", "=", command.args] if is_id else ["search", "=", command.args]
res = await fetch_vndb("character", filt, "id, name, description, gender, image{url}") fields = "id, name, original, description, gender, age, blood_type, image{url}"
res = await fetch_vndb("character", filt, fields)
if not res: return await message.answer("❌ Not found.") if not res: return await message.answer("❌ Not found.")
c = res[0]
text = (f"<b>NAME:</b> {c['name']}\n<b>GENDER:</b> {str(c.get('gender')).upper()}\n\n"
f"<b>DESC:</b>\n<i>{clean_text(c.get('description'), 500)}</i>\n\n"
f"<b>VNDB:</b> https://vndb.org/{c['id']}")
await send_result(message, text, c.get('image', {}).get('url'))
@dp.message(Command("start", "help")) target = res[0]
async def cmd_start(message: types.Message): if not is_id and len(res) > 1:
await message.answer( exact = find_exact_match(res, command.args, 'name')
"🤖 <b>VNDB Pro Bot</b>\n\n" if exact: target = exact
"<b>Explore:</b>\n" else:
"/vn [name/id] - Search & Details\n" out = [f"{i['name']} — <code>{i['id']}</code>" for i in res[:10]]
"/char [name/id] - Characters\n" return await message.answer("👤 <b>Select Character:</b>\n" + "\n".join(out))
"/random - Get a random high-rated VN\n"
"/top - Show Top 10 Visual Novels\n" img = target.get('image', {}).get('url') if target.get('image') else None
"/release [name/id] - Release info" raw_gender = target.get('gender')
if isinstance(raw_gender, list): raw_gender = raw_gender[0] if raw_gender else None
text = (
f"<b>NAME:</b> {target['name']}\n"
f"<b>ORIGINAL:</b> {target.get('original') or 'N/A'}\n"
f"<b>GENDER:</b> {str(raw_gender).upper()}\n"
f"<b>AGE:</b> {target.get('age') or 'N/A'} | <b>BLOOD:</b> {target.get('blood_type') or 'N/A'}\n\n"
f"<b>DESCRIPTION:</b>\n<i>{clean_text(target.get('description'), 800)}</i>\n\n"
f"<b>VNDB:</b> https://vndb.org/{target['id']}"
) )
await send_result(message, text, img)
# Стаб для release (использует логику из предыдущего ответа)
@dp.message(Command("release")) @dp.message(Command("release"))
async def handle_rel_wrap(message: types.Message, command: CommandObject): async def handle_release(message: types.Message, command: CommandObject):
# (здесь остается логика из предыдущего кода для краткости опустим) if not command.args: return await message.answer("Usage: /release <name/id>")
pass
is_id = command.args.startswith('r') and command.args[1:].isdigit()
filt = ["id", "=", command.args] if is_id else ["search", "=", command.args]
fields = "id, title, alttitle, released, languages{lang}, platforms, extlinks{url, label}, images{url}"
res = await fetch_vndb("release", filt, fields)
if not res: return await message.answer("❌ Not found.")
target = res[0]
if not is_id and len(res) > 1:
exact = find_exact_match(res, command.args, 'title')
if exact: target = exact
else:
out = [f"{i['title']} — <code>{i['id']}</code>" for i in res[:10]]
return await message.answer("💿 <b>Select Release:</b>\n" + "\n".join(out))
img_list = target.get('images', [])
img = img_list[0].get('url') if img_list else None
links = "\n".join([f"• <a href='{l['url']}'>{l['label']}</a>" for l in target.get('extlinks', [])[:8]])
text = (
f"<b>RELEASE:</b> {target['title']}\n"
f"<b>ORIGINAL:</b> {target.get('alttitle') or 'N/A'}\n\n"
f"<b>DATE:</b> {target.get('released', 'N/A')}\n"
f"<b>LANGS:</b> {safe_list_to_str(target.get('languages'))}\n"
f"<b>PLATFORMS:</b> {safe_list_to_str(target.get('platforms'))}\n\n"
f"<b>LINKS / STORES:</b>\n{links or 'N/A'}\n\n"
f"<b>VNDB:</b> https://vndb.org/{target['id']}"
)
await send_result(message, text, img)
@dp.message(Command("random"))
async def handle_random(message: types.Message):
res = await fetch_vndb("vn", ["rating", ">=", 80], "id, title, image{url}, rating, description", sort="votecount", results=100)
if not res: return await message.answer("❌ API error")
v = random.choice(res)
text = f"🎲 <b>RANDOM VN</b>\n\n<b>{v['title']}</b>\nRating: {v['rating']/10}\n\n<i>{clean_text(v.get('description'), 400)}</i>\n\nhttps://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):
res = await fetch_vndb("vn", ["votecount", ">", 2000], "id, title, rating", sort="rating", results=10)
out = ["🏆 <b>TOP 10 VISUAL NOVELS</b>"]
for i, v in enumerate(res, 1):
out.append(f"{i}. {v['title']} — <b>{v['rating']/10}</b>")
await message.answer("\n".join(out))
@dp.message(Command("start", "help", "search"))
async def cmd_start(message: types.Message):
await message.answer("🤖 <b>VNDB Professional</b>\n/vn, /char, /release, /random, /top")
if __name__ == "__main__": if __name__ == "__main__":
dp.run_polling(bot) dp.run_polling(bot)