feat: rapid/blitz ratings, instant addplayer, hyperlinks, bug fixes
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 7s

- fetch_fide_player: parse rapid and blitz ratings from FIDE profile
- subscriptions: add rapid/blitz columns (auto-migration)
- /myplayers card: show rapid (📍) and blitz () ratings
- /addplayer confirmation: show rapid/blitz alongside standard rating
- find_player_in_cache: extract instant SQL-only search from scan_for_player
- /addplayer: use find_player_in_cache instead of full HTTP scan — instant response
- tournament names in /myplayers and /addplayer are now hyperlinks
- add_player_watch: ON CONFLICT DO UPDATE to reactivate deleted players
- my_players: try/except per card with plain-text fallback, error logged to stderr

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
vrubel 2026-06-20 23:35:49 +00:00
parent 39f1f7e857
commit 66b10d9425
2 changed files with 92 additions and 43 deletions

View file

@ -332,8 +332,16 @@ def _player_card(p: dict, loc: dict) -> tuple[str, InlineKeyboardMarkup]:
rating = p['rating'] or ''
fide_id = p['fide_id']
rapid = p.get('rapid') or 0
blitz = p.get('blitz') or 0
ratings = str(rating)
if rapid:
ratings += f' 📍{rapid}'
if blitz:
ratings += f'{blitz}'
lines = [f'👤 *{safe_name}*']
lines.append(f'🏳 {_md_escape(fed)} · {rating}')
lines.append(f'🏳 {_md_escape(fed)} · {_md_escape(ratings)}')
tours = p['tournaments']
waiting = all(t['name'] is None for t in tours)
@ -343,10 +351,13 @@ def _player_card(p: dict, loc: dict) -> tuple[str, InlineKeyboardMarkup]:
for t in tours:
if t['name'] is None:
continue
safe_tour = _md_escape(t['name'])
pts = _md_escape(f'{t["points"]:.1f}')
rd = t['round_done']
lines.append(f'📌 {safe_tour}')
if t.get('url'):
tour_link = f'[{_md_escape(t["name"])}]({t["url"]})'
else:
tour_link = _md_escape(t['name'])
lines.append(f'📌 {tour_link}')
lines.append(f' {pts} {loc["points_label"]}, {loc["round_label"]} {rd}')
keyboard = InlineKeyboardMarkup([[
@ -368,8 +379,17 @@ async def my_players(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(header, parse_mode=ParseMode.MARKDOWN_V2)
for p in players:
text, keyboard = _player_card(p, loc)
await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN_V2,
reply_markup=keyboard)
try:
await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN_V2,
reply_markup=keyboard,
disable_web_page_preview=True)
except Exception as e:
import sys
print(f'my_players card error: {e}\ntext={text!r}', file=sys.stderr)
# Fallback: send as plain text without formatting
plain = text.replace('\\', '').replace('*', '').replace('_', '')
await update.message.reply_text(plain, reply_markup=keyboard,
disable_web_page_preview=True)
async def remove_player_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
@ -422,18 +442,20 @@ async def _lookup_and_confirm(update: Update, context, fide_id: int):
title = player.get('title') or ''
title_str = f', {_md_escape(title)}' if title and title != 'None' else ''
fed = player.get('fed') or ''
ratings_parts = [f'Рейтинг: {player["rating"]}']
if player.get('rapid'):
ratings_parts.append(f'📍{player["rapid"]}')
if player.get('blitz'):
ratings_parts.append(f'{player["blitz"]}')
ratings_str = _md_escape(' '.join(ratings_parts))
await update.message.reply_text(
f'✅ *{safe_name}*{title_str}\n'
f'{"🇫 " if fed else ""}{_md_escape(fed)} \\| Рейтинг: {player["rating"]}\n\n'
f'{"🇫 " if fed else ""}{_md_escape(fed)} \\| {ratings_str}\n\n'
'🔍 Ищу турниры на chess\\-results\\.com\\.\\.',
parse_mode=ParseMode.MARKDOWN_V2)
# Run scan asynchronously (avoid blocking the event loop)
# Use canonical FIDE ID from profile page (may differ from URL-extracted ID)
from concurrent.futures import ThreadPoolExecutor
loop = asyncio.get_event_loop()
tournaments = await loop.run_in_executor(
None, tracker.scan_for_player, player['fide_id'])
# SQL-only cache search (instant, no HTTP). Background jobs handle HTTP discovery.
tournaments = tracker.find_player_in_cache(player['fide_id'])
if tournaments:
# Auto-subscribe to all found tournaments
@ -445,8 +467,8 @@ async def _lookup_and_confirm(update: Update, context, fide_id: int):
dates = ''
if t['start_date'] and t['end_date']:
dates = f' \\({_md_escape(t["start_date"])}{_md_escape(t["end_date"])}\\)'
safe_tour = _md_escape(t['name'])
lines.append(f'{safe_tour}{dates}')
tour_link = f'[{_md_escape(t["name"])}]({t["url"]})'
lines.append(f'{tour_link}{dates}')
lines.append('\nЯ буду присылать результаты и жеребьёвку\.\n'
'/myplayers \\- список подписок')
await update.message.reply_text('\n'.join(lines),