diff --git a/bots/tracker.py b/bots/tracker.py index b265d61..14bb0a0 100644 --- a/bots/tracker.py +++ b/bots/tracker.py @@ -524,14 +524,21 @@ def _parse_art0_page(html: str) -> dict: rating = 0 name = '' for ci, t in enumerate(texts): - if re.match(r'^\d{6,10}$', t): + # Only capture the FIRST 6-10 digit number as FIDE ID. + # Subsequent matches are national IDs — must not overwrite. + if re.match(r'^\d{6,10}$', t) and not fide_id: fide_id = int(t) - if ci + 1 < len(texts) and re.match(r'^[A-Z]{3}$', texts[ci + 1]): - fed = texts[ci + 1] - if ci + (2 if fed else 1) < len(texts): - rt = texts[ci + (2 if fed else 1)] - if rt.isdigit(): - rating = int(rt) + # Scan forward for 3-letter FED code (skipping national ID) + for look in range(1, min(5, len(texts) - ci)): + nt = texts[ci + look] + if re.match(r'^[A-Z]{3}$', nt): + fed = nt + # Rating is right after FED + if ci + look + 1 < len(texts): + rt = texts[ci + look + 1] + if rt.isdigit() and int(rt) <= 4000: + rating = int(rt) + break if ci >= 1 and re.search(r'[A-Za-zА-Яа-я]', t) and len(t) > 2: if not name: name = t @@ -818,6 +825,21 @@ async def rescan_existing_tournaments(context): continue # tournament deleted or unreachable new_fids = set(new_data['players'].keys()) + # Correct player_sno for subscriptions whose SNo drifted from the cache + tnr_url_frag = f'tnr{tnr}.aspx' + for fid in new_fids: + if fid not in fide_to_users: + continue + correct_sno = new_data['players'][fid]['sno'] + conn = _get_conn() + conn.execute( + 'UPDATE subscriptions SET player_sno = ? ' + 'WHERE fide_id = ? AND tournament_url LIKE ? ' + 'AND player_sno != ? AND active = 1', + (correct_sno, fid, f'%{tnr_url_frag}%', correct_sno)) + conn.commit() + conn.close() + # New players that appeared since last cached snapshot appeared = new_fids - old_fids for fid in appeared: diff --git a/swiss_calc/parser.py b/swiss_calc/parser.py index c90a848..bde19ec 100644 --- a/swiss_calc/parser.py +++ b/swiss_calc/parser.py @@ -413,27 +413,27 @@ def fetch_tournament(url: str) -> dict: for s in standings: sname = _normalize_name(s['name']) - # Try exact match first + # Exact name match: safe to trust both SNo and rating from start list. real_sno = name_to_sno.get(sname) - if real_sno is None: - # Fuzzy: find best partial match (first+last name overlap) + if real_sno: + s['starting_sno'] = real_sno + s['rating'] = start_players[real_sno].get('rating', s.get('rating', 0)) + else: + # Fuzzy match is used ONLY for rating lookup — never overrides starting_sno. + # (starting_sno from column 0 of art=4 standings is already the correct + # starting number; overwriting it with a fuzzy-matched SNo corrupts + # the opponent history and leads to wrong pairings.) sname_parts = set(sname.split()) - best_sno = None - best_overlap = 0 + best_sno, best_overlap = None, 0 for sl_name, sl_sno in name_to_sno.items(): sl_parts = set(sl_name.split()) overlap = len(sname_parts & sl_parts) if overlap > best_overlap: - best_overlap = overlap - best_sno = sl_sno - real_sno = best_sno + best_overlap, best_sno = overlap, sl_sno + if best_sno and best_sno in start_players: + s['rating'] = start_players[best_sno].get('rating', s.get('rating', 0)) - s['starting_sno'] = real_sno if real_sno else s.get('starting_sno', s['rank']) - - # Get real rating from start list - if real_sno and real_sno in start_players: - s['rating'] = start_players[real_sno].get('rating', s.get('rating', 0)) - elif 'rating' not in s: + if 'rating' not in s: s['rating'] = 0 # Keep original results from standings as backup (for byes/forfeits)