fix: auto-tournament search via local tnr_cache index

- Extract canonical FIDE ID from profile page (profile-info-id)
- Add tnr_cache table for local tournament index with players
- scan_for_player uses cache-only search (instant SQL, 0 HTTP)
- warmup_cache builds full index at step 1 (1445000→1434000)
- Save/resume warmup progress via tnr_state
- Auto-migrate missing columns (start_date/end_date)
- Add hourly rescan_new_tournaments job
- Fix discover_max_tnr (larger probe steps, lower default)
- Use asyncio.to_thread() for warmup (no crutches)
This commit is contained in:
vrubel 2026-06-20 07:15:16 +00:00
parent 7ebe1aa66e
commit 89f5861ee3
2 changed files with 514 additions and 18 deletions

View file

@ -262,9 +262,10 @@ async def my_players(update: Update, context: ContextTypes.DEFAULT_TYPE):
for s in subs:
safe_name = _md_escape(s['player_name'])
safe_tour = _md_escape(s['tournament_name'] or '')
last_pts_str = _md_escape(f'{s["last_points"]:.1f}')
lines.append(
f' {s["id"]}\\. {safe_name} \({s["rating"]}, {s["fed"]}\) '
f'{safe_tour}{s["last_points"]:.1f} очк\\.')
f'{safe_tour}{last_pts_str} очк\\.')
lines.append('\n/removeplayer <номер> — удалить подписку')
await update.message.reply_text('\n'.join(lines),
parse_mode=ParseMode.MARKDOWN_V2)
@ -315,19 +316,47 @@ async def _lookup_and_confirm(update: Update, context, fide_id: int):
safe_name = _md_escape(player['name'])
title_str = f', {player["title"]}' if player.get('title') else ''
msg = (
await update.message.reply_text(
f'✅ *{safe_name}*{title_str}\n'
f'{"🇫 " if player["fed"] else ""}{player["fed"]} \\| Рейтинг: {player["rating"]}\n\n'
'Теперь отправь ссылку на турнир chess\\-results\\.com, '
'где играет этот игрок\.\n'
'/cancel — отмена'
)
context.user_data['pending_fide'] = {
'player': player,
'lang': lang_code,
}
await update.message.reply_text(msg, parse_mode=ParseMode.MARKDOWN_V2)
'🔍 Ищу турниры на 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'])
if tournaments:
# Auto-subscribe to all found tournaments
lines = [f'✅ *Найдено турниров: {len(tournaments)}*']
for t in tournaments:
tracker.add_subscription(
user.id, player, t['url'], t['name'], t['sno'],
0, 0.0, lang_code, t['start_date'], t['end_date'])
dates = ''
if t['start_date'] and t['end_date']:
dates = f' \({t["start_date"]}{t["end_date"]}\)'
safe_tour = _md_escape(t['name'])
lines.append(f'{safe_tour}{dates}')
lines.append('\nЯ буду присылать результаты и жеребьёвку\.\n'
'/myplayers — список подписок')
await update.message.reply_text('\n'.join(lines),
parse_mode=ParseMode.MARKDOWN_V2)
else:
# No tournaments found — fallback to manual URL
context.user_data['pending_fide'] = {
'player': player,
'lang': lang_code,
}
await update.message.reply_text(
'❌ Турниров не найдено автоматически\\.\n\n'
'Отправь ссылку на турнир chess\\-results\\.com, '
'где играет этот игрок\\.\n'
'/cancel — отмена',
parse_mode=ParseMode.MARKDOWN_V2)
def _apply_round_results(tournament, games, rd):
@ -442,7 +471,7 @@ async def _subscribe_to_tournament(update: Update, context, url: str, pending: d
await msg.edit_text(
f'✅ *{safe_name}* найден в турнире\\.\n'
f'📅 *{safe_tour}*\n'
f'Ст\\.№ {p_sno}, {points:.1f} очк\\. — отслеживаю\!\n\n'
f'Ст\\.№ {p_sno}, {_md_escape(f"{points:.1f}")} очк\\. — отслеживаю\!\n\n'
'Я буду присылать результаты игрока и жеребьёвку следующих туров\.\n'
'/myplayers — список подписок',
parse_mode=ParseMode.MARKDOWN_V2)
@ -630,6 +659,12 @@ def main():
app.job_queue.run_repeating(
tracker.check_all_subscriptions, interval=300, first=10)
app.job_queue.run_repeating(
tracker.rescan_new_tournaments, interval=3600, first=60)
app.job_queue.run_repeating(
tracker.warmup_cache, interval=21600, first=10)
print('Client bot started', file=sys.stderr)
print('Client bot started', file=sys.stderr)
app.run_polling()