fix: wrong player highlighted and wrong pairings due to FIDE ID / SNo mismatch
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 8s
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 8s
- tracker.py _parse_art0_page: stop at first 6-10 digit number as FIDE ID; national IDs (also 6-10 digits) no longer overwrite it. FED/rating now scanned forward past the national ID column. - parser.py fetch_tournament: fuzzy start-list name matching no longer overrides starting_sno from standings column 0. Fuzzy match is kept only for rating lookup. Exact name match still may override SNo (reliable). - tracker.py rescan_existing_tournaments: after each art=0 re-fetch, update player_sno in subscriptions whose FIDE ID appears at a different SNo than stored (corrects drift caused by the old parsing bug). Root cause: national ID of Gorshkov (SNo=26 in Odintsovo Blitz) matched Vrubel's FIDE ID in the cache; subscription was created with wrong player_sno=26. DB already patched directly: Vrubel→sno=39, Aslanbekov→sno=40. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8058080c3a
commit
e64e436b6f
2 changed files with 43 additions and 21 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue