Улучшение обработки команд: изменение лимита символов в функции clean_text, рефакторинг функции safe_list_to_str, добавление логики поиска точного совпадения и обновление формата ответов

This commit is contained in:
2026-05-01 18:47:26 +03:00
parent 1cc82a133a
commit 82afe8c653

172
bot.py
View File

@@ -2,6 +2,7 @@ import os
import logging import logging
import re import re
import httpx import httpx
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
@@ -18,12 +19,14 @@ dp = Dispatcher()
# --- UTILS --- # --- UTILS ---
def clean_text(text: str) -> 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[:600] + "..." if len(text) > 600 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"
@@ -32,24 +35,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') # Ищем любое человекочитаемое поле
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 send_result(message: types.Message, text: str, image_url: str = None): 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}
try:
if image_url:
await message.answer_photo(photo=image_url, caption=text[:1024])
else:
await message.answer(text)
except Exception as e:
logger.warning(f"Failed to send photo: {e}. Falling back to text.")
await message.answer(text)
async def fetch_vndb(endpoint: str, filters: list, fields: str):
payload = {"filters": filters, "fields": fields}
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)
@@ -58,7 +51,23 @@ async def fetch_vndb(endpoint: str, filters: list, fields: str):
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
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)
def find_exact_match(results, query, attr):
"""Ищет 100% совпадение по имени/названию в результатах."""
for item in results:
if item.get(attr, "").lower() == query.lower():
return item
return None return None
# --- HANDLERS --- # --- HANDLERS ---
@@ -69,28 +78,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}"
# image{url} - правильный синтаксис для Kana API
fields = "id, title, alttitle, released, rating, votecount, description, languages, platforms, developers{name}, image{url}"
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:
out = [f"{i['title']} — <code>{i['id']}</code>" for i in res[:10]]
return await message.answer("🔍 <b>Select VN by ID:</b>\n" + "\n".join(out))
v = res[0] # Логика умного поиска
img = v.get('image', {}).get('url') if v.get('image') else None 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 VN:</b>\n" + "\n".join(out))
img = target.get('image', {}).get('url') if target.get('image') else None
rel_str = "\n".join([f"{r['title']} (<code>{r['id']}</code>) - {r['relation']}" for r in target.get('relations', [])[:3]])
text = ( text = (
f"<b>TITLE:</b> {v['title']}\n" f"<b>TITLE:</b> {target['title']}\n"
f"<b>ORIGINAL:</b> {v.get('alttitle') or 'N/A'}\n" f"<b>ORIGINAL:</b> {target.get('alttitle') or 'N/A'}\n"
f"<b>DEVELOPER:</b> {safe_list_to_str(v.get('developers'))}\n" f"<b>DEVELOPER:</b> {safe_list_to_str(target.get('developers'))}\n"
f"<b>RATING:</b> {v['rating']/10 if v.get('rating') else 'N/A'} ({v.get('votecount', 0)} votes)\n" f"<b>RATING:</b> {target['rating']/10 if target.get('rating') else 'N/A'} ({target.get('votecount', 0)} votes)\n"
f"<b>LANGUAGES:</b> {safe_list_to_str(v.get('languages'))}\n" f"<b>LANGS:</b> {safe_list_to_str(target.get('languages'))}\n"
f"<b>PLATFORMS:</b> {safe_list_to_str(v.get('platforms'))}\n\n" f"<b>PLATFORMS:</b> {safe_list_to_str(target.get('platforms'))}\n\n"
f"<b>DESCRIPTION:</b>\n<i>{clean_text(v.get('description'))}</i>\n\n" f"<b>RELATIONS:</b>\n{rel_str or 'None'}\n\n"
f"<b>VNDB:</b> https://vndb.org/{v['id']}" 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)
@@ -100,67 +115,86 @@ async def handle_char(message: types.Message, command: CommandObject):
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]
fields = "id, name, original, description, gender, age, blood_type, image{url}" fields = "id, name, original, description, gender, age, blood_type, image{url}"
res = await fetch_vndb("character", filt, fields) 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.")
if len(res) > 1 and not is_id:
out = [f"{i['name']} — <code>{i['id']}</code>" for i in res[:10]]
return await message.answer("👤 <b>Select Character by ID:</b>\n" + "\n".join(out))
c = res[0] target = res[0]
img = c.get('image', {}).get('url') if c.get('image') else None if not is_id and len(res) > 1:
raw_gender = c.get('gender') exact = find_exact_match(res, command.args, 'name')
if exact: target = exact
else:
out = [f"{i['name']} — <code>{i['id']}</code>" for i in res[:10]]
return await message.answer("👤 <b>Select Character:</b>\n" + "\n".join(out))
img = target.get('image', {}).get('url') if target.get('image') else None
raw_gender = target.get('gender')
if isinstance(raw_gender, list): raw_gender = raw_gender[0] if raw_gender else None if isinstance(raw_gender, list): raw_gender = raw_gender[0] if raw_gender else None
text = ( text = (
f"<b>NAME:</b> {c['name']}\n" f"<b>NAME:</b> {target['name']}\n"
f"<b>ORIGINAL:</b> {c.get('original') or 'N/A'}\n" f"<b>ORIGINAL:</b> {target.get('original') or 'N/A'}\n"
f"<b>GENDER:</b> {str(raw_gender).upper()}\n" f"<b>GENDER:</b> {str(raw_gender).upper()}\n"
f"<b>AGE:</b> {c.get('age') or 'N/A'}\n\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(c.get('description'))}</i>\n\n" f"<b>DESCRIPTION:</b>\n<i>{clean_text(target.get('description'), 800)}</i>\n\n"
f"<b>VNDB:</b> https://vndb.org/{c['id']}" f"<b>VNDB:</b> https://vndb.org/{target['id']}"
) )
await send_result(message, text, img) await send_result(message, text, img)
@dp.message(Command("release")) @dp.message(Command("release"))
async def handle_rel(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>") if not command.args: return await message.answer("Usage: /release <name/id>")
is_id = command.args.startswith('r') and command.args[1:].isdigit() is_id = command.args.startswith('r') 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]
# images{url} для релизов (это список)
fields = "id, title, alttitle, released, languages{lang}, platforms, extlinks{url, label}, images{url}" fields = "id, title, alttitle, released, languages{lang}, platforms, extlinks{url, label}, images{url}"
res = await fetch_vndb("release", filt, fields) res = await fetch_vndb("release", 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 Release by ID:</b>\n" + "\n".join(out)) return await message.answer("💿 <b>Select Release:</b>\n" + "\n".join(out))
r = res[0] img_list = target.get('images', [])
# Берем первое изображение из списка images
img_list = r.get('images', [])
img = img_list[0].get('url') if img_list else None 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]])
links = "\n".join([f"• <a href='{l['url']}'>{l['label']}</a>" for l in r.get('extlinks', [])[:5]])
text = ( text = (
f"<b>RELEASE:</b> {r['title']}\n" f"<b>RELEASE:</b> {target['title']}\n"
f"<b>ORIGINAL:</b> {r.get('alttitle') or 'N/A'}\n" f"<b>ORIGINAL:</b> {target.get('alttitle') or 'N/A'}\n\n"
f"<b>DATE:</b> {r.get('released', 'N/A')}\n" f"<b>DATE:</b> {target.get('released', 'N/A')}\n"
f"<b>LANGUAGES:</b> {safe_list_to_str(r.get('languages'))}\n" f"<b>LANGS:</b> {safe_list_to_str(target.get('languages'))}\n"
f"<b>PLATFORMS:</b> {safe_list_to_str(r.get('platforms'))}\n\n" f"<b>PLATFORMS:</b> {safe_list_to_str(target.get('platforms'))}\n\n"
f"<b>STORES:</b>\n{links or 'N/A'}\n\n" f"<b>LINKS / STORES:</b>\n{links or 'N/A'}\n\n"
f"<b>VNDB:</b> https://vndb.org/{r['id']}" f"<b>VNDB:</b> https://vndb.org/{target['id']}"
) )
await send_result(message, text, img) await send_result(message, text, img)
@dp.message(Command("start", "help")) @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): async def cmd_start(message: types.Message):
await message.answer("🤖 <b>VNDB Bot with Posters</b>\n\n/vn [name/id]\n/char [name/id]\n/release [name/id]") 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)