diff --git a/bots/client_bot.py b/bots/client_bot.py index 74f581c..ffa11c0 100644 --- a/bots/client_bot.py +++ b/bots/client_bot.py @@ -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), diff --git a/bots/tracker.py b/bots/tracker.py index 47d3eca..dbfa62f 100644 --- a/bots/tracker.py +++ b/bots/tracker.py @@ -89,6 +89,8 @@ def _get_conn(): # Migrate existing tables that may be missing columns added in later versions _migrate_add_column(conn, 'subscriptions', 'start_date', "TEXT DEFAULT ''") _migrate_add_column(conn, 'subscriptions', 'end_date', "TEXT DEFAULT ''") + _migrate_add_column(conn, 'subscriptions', 'rapid', "INTEGER DEFAULT 0") + _migrate_add_column(conn, 'subscriptions', 'blitz', "INTEGER DEFAULT 0") conn.commit() return conn @@ -121,12 +123,22 @@ def fetch_fide_player(fide_id: int) -> dict: if m: fed = m.group(1) - # Rating (standard):
2841
STANDARD + # Ratings:
2841
STANDARD / RAPID / BLITZ rating = 0 m = re.search(r'
(\d{3,4})
\s*]*>\s*STANDARD', html) if m: rating = int(m.group(1)) + rapid = 0 + m = re.search(r'
(\d{3,4})
\s*]*>\s*RAPID', html) + if m: + rapid = int(m.group(1)) + + blitz = 0 + m = re.search(r'
(\d{3,4})
\s*]*>\s*BLITZ', html) + if m: + blitz = int(m.group(1)) + # Title:
Grandmaster
title = '' m = re.search(r'(.*?)
', html) @@ -142,6 +154,7 @@ def fetch_fide_player(fide_id: int) -> dict: return { 'fide_id': fide_id, 'name': name, 'rating': rating, + 'rapid': rapid, 'blitz': blitz, 'fed': fed, 'title': title, } @@ -156,13 +169,19 @@ def add_player_watch(user_id: int, player: dict, lang: str): """ conn = _get_conn() conn.execute(''' - INSERT OR IGNORE INTO subscriptions - (user_id, fide_id, player_name, rating, fed, lang, tournament_url, + INSERT INTO subscriptions + (user_id, fide_id, player_name, rating, rapid, blitz, fed, lang, tournament_url, tournament_name, player_sno, last_results_count, last_points, last_round_done, active) - VALUES (?, ?, ?, ?, ?, ?, 'fide_only', '', 0, 0, 0.0, 0, 1) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'fide_only', '', 0, 0, 0.0, 0, 1) + ON CONFLICT(user_id, fide_id, tournament_url) DO UPDATE SET + active = 1, + player_name = excluded.player_name, + rating = excluded.rating, + rapid = excluded.rapid, + blitz = excluded.blitz ''', (user_id, player['fide_id'], player['name'], player.get('rating', 0), - player.get('fed', ''), lang)) + player.get('rapid', 0), player.get('blitz', 0), player.get('fed', ''), lang)) conn.commit() conn.close() @@ -174,12 +193,13 @@ def add_subscription(user_id: int, player: dict, tournament_url: str, conn = _get_conn() conn.execute(''' INSERT OR REPLACE INTO subscriptions - (user_id, fide_id, player_name, rating, fed, lang, tournament_url, + (user_id, fide_id, player_name, rating, rapid, blitz, fed, lang, tournament_url, tournament_name, player_sno, last_results_count, last_points, last_round_done, start_date, end_date, active) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, 1) - ''', (user_id, player['fide_id'], player['name'], player['rating'], - player.get('fed', ''), lang, tournament_url, tournament_name, + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, 1) + ''', (user_id, player['fide_id'], player['name'], player.get('rating', 0), + player.get('rapid', 0), player.get('blitz', 0), player.get('fed', ''), + lang, tournament_url, tournament_name, player_sno, last_results_count, last_points, start_date, end_date)) conn.commit() conn.close() @@ -189,7 +209,7 @@ def get_user_subs(user_id: int) -> list: """Return subscriptions grouped by player (fide_id).""" conn = _get_conn() rows = conn.execute( - 'SELECT fide_id, player_name, rating, fed, tournament_name, ' + 'SELECT fide_id, player_name, rating, rapid, blitz, fed, tournament_name, ' 'last_points, last_round_done, tournament_url FROM subscriptions ' 'WHERE user_id = ? AND active = 1 ORDER BY fide_id, id', (user_id,)).fetchall() @@ -197,20 +217,21 @@ def get_user_subs(user_id: int) -> list: players: dict = {} for r in rows: - fide_id, pname, rating, fed, tname, pts, rd, url = r + fide_id, pname, rating, rapid, blitz, fed, tname, pts, rd, url = r if fide_id not in players: players[fide_id] = { 'fide_id': fide_id, 'player_name': pname, - 'rating': rating, 'fed': fed or '', + 'rating': rating, 'rapid': rapid or 0, 'blitz': blitz or 0, + 'fed': fed or '', 'tournaments': [], } if url == 'fide_only': players[fide_id]['tournaments'].append({ - 'name': None, 'points': pts, 'round_done': rd, + 'name': None, 'url': None, 'points': pts, 'round_done': rd, }) else: players[fide_id]['tournaments'].append({ - 'name': tname or url, 'points': pts, 'round_done': rd, + 'name': tname or url, 'url': url, 'points': pts, 'round_done': rd, }) return list(players.values()) @@ -578,27 +599,16 @@ def discover_max_tnr() -> int: return tnr -def scan_for_player(fide_id: int, max_tnr: int = None, - limit: int = 200) -> list: - """Scan tournaments for a player by FIDE ID. - - Scans multiple TNR windows since chess-results numbering has sparse clusters. - Returns list of {tnr, url, name, start_date, end_date, sno, player_name, fide_id}. - """ - import time - - results = [] - seen_tnrs = set() - - # Phase 1: check full tnr_cache via SQL (single query, instant). Return if found. +def find_player_in_cache(fide_id: int) -> list: + """Instant SQL-only search. Returns same format as scan_for_player.""" conn = _get_conn() pattern = f'%"{fide_id}"%' rows = conn.execute( 'SELECT tnr, name, start_date, end_date, players_json FROM tnr_cache ' - 'WHERE players_json LIKE ? ' - 'ORDER BY tnr DESC', + 'WHERE players_json LIKE ? ORDER BY tnr DESC', (pattern,)).fetchall() conn.close() + results = [] for row in rows: tnr, tname, tstart, tend, players_json = row players = {int(k): v for k, v in json.loads(players_json).items()} @@ -617,6 +627,23 @@ def scan_for_player(fide_id: int, max_tnr: int = None, }) if results: set_tnr_state('max_tnr_seen', str(max(r['tnr'] for r in results))) + return results + + +def scan_for_player(fide_id: int, max_tnr: int = None, + limit: int = 200) -> list: + """Scan tournaments for a player by FIDE ID (SQL cache + HTTP fallback). + + Used by background jobs. For user-facing flows use find_player_in_cache(). + """ + import time + + results = [] + seen_tnrs = set() + + # Phase 1: SQL cache (instant) + results = find_player_in_cache(fide_id) + if results: return results # Phase 2: cache miss — fall back to sequential TNR scan