#!/usr/bin/env python3 """BGG Author Fact-Check — prüft Autoren-Zuordnungen gegen BoardGameGeek. Extrahiert Spiel-Autor-Paare aus Artikel-HTML und verifiziert über die BGG XML API v2, ob der genannte Autor tatsächlich Designer des Spiels ist. Usage: python3 check_authors.py Exit 0 = alle Autoren korrekt, 1 = Fehler gefunden, 2 = API/Warning """ import sys, re, urllib.request, urllib.parse, xml.etree.ElementTree as ET, time BGG_KEY_FILE = "/tmp/.bggkey" def get_bgg_key(): try: with open(BGG_KEY_FILE) as f: return f.read().strip() except FileNotFoundError: return None def bgg_search(game_name): """Search BGG for a game, return results with exact matches first.""" key = get_bgg_key() url = f"https://boardgamegeek.com/xmlapi2/search?query={urllib.parse.quote(game_name)}&type=boardgame" headers = {} if key: headers["Authorization"] = f"Bearer {key}" req = urllib.request.Request(url, headers=headers) for attempt in range(3): try: resp = urllib.request.urlopen(req, timeout=15) if resp.getcode() == 202: time.sleep(3) continue root = ET.fromstring(resp.read()) results = [] for item in root.findall("item"): results.append({ "id": item.attrib.get("id"), "name": item.find("name").attrib.get("value", ""), "year": item.find("yearpublished").attrib.get("value", "") if item.find("yearpublished") is not None else "" }) # Sort: exact name matches first exact = [r for r in results if r["name"].lower() == game_name.lower()] others = [r for r in results if r["name"].lower() != game_name.lower()] return exact + others except Exception: time.sleep(2) return [] def bgg_thing(game_id): """Get BGG thing data including designers.""" key = get_bgg_key() url = f"https://boardgamegeek.com/xmlapi2/thing?id={game_id}&stats=1" headers = {} if key: headers["Authorization"] = f"Bearer {key}" req = urllib.request.Request(url, headers=headers) for attempt in range(3): try: resp = urllib.request.urlopen(req, timeout=15) if resp.getcode() == 202: time.sleep(3) continue root = ET.fromstring(resp.read()) item = root.find("item") if item is None: return None designers = [] for link in item.findall("link"): if link.attrib.get("type") == "boardgamedesigner": designers.append(link.attrib.get("value", "")) return { "name": item.find("name").attrib.get("value", "") if item.find("name") is not None else "", "designers": designers, "year": item.find("yearpublished").attrib.get("value", "") if item.find("yearpublished") is not None else "" } except Exception: time.sleep(2) return None def extract_game_author_pairs(html): """Extrahiert Spiel-Autor-Paare über BGG-Links + Infoboxen im Artikel.""" pairs = [] # Finde alle Infoboxen mit BGG-Link # Pattern:

GAME · AUTHOR · ... infobox_pattern = re.compile( r'

\s*]*>' r'([^<]+)\s*·\s*' # Game name r'([^·<]+)' # Author (stop at next · or <) r'.*?boardgamegeek\.com/boardgame/(\d+)/', # BGG ID re.DOTALL ) for m in infobox_pattern.finditer(html): game = m.group(1).strip() author_raw = m.group(2).strip() bgg_id = m.group(3) # Clean author: remove nested tags author = re.sub(r'<[^>]+>', '', author_raw).strip() pairs.append({ "game": game, "claimed_author": author, "bgg_id": bgg_id, "context": m.group(0)[:200] }) # Pattern 2: Prose claims — find "für ... GAME" and backtrack for author prose_pattern = re.compile( r'(?:gewann|erhielt|bekam|holte).*?f\xfcr\s+.*?([^<]+)', re.DOTALL | re.IGNORECASE ) for m in prose_pattern.finditer(html): game = m.group(1).strip() # Skip if already found in infobox check if any(p["game"] == game for p in pairs): continue # Backtrack up to 500 chars to find preceding AUTHOR start = max(0, m.start() - 500) before = html[start:m.start()] # Find the LAST before this match (likely the author) author_matches = re.findall(r'([^<]+)', before) if author_matches: author = author_matches[-1].strip() # Search BGG for this game results = bgg_search(game) if results: pairs.append({ "game": game, "claimed_author": author, "bgg_id": results[0]["id"], "context": f"...{author}...gewann...f\xfcr {game}..." }) return pairs def normalize_name(name): """Normalize author name for comparison.""" return re.sub(r'\s+', ' ', name.strip().lower()) def main(): if len(sys.argv) < 2: print("Usage: check_authors.py ") sys.exit(1) with open(sys.argv[1]) as f: html = f.read() # Extract body body_match = re.search(r'
(.*?)
', html, re.DOTALL) body = body_match.group(1) if body_match else html pairs = extract_game_author_pairs(body) if not pairs: print("ℹ️ Keine Spiel-Autor-Paare zur Prüfung gefunden.") sys.exit(0) errors = 0 warnings = 0 for pair in pairs: game = pair["game"] print(f"\n🔍 Prüfe: »{game}«") # Use BGG ID directly if available, otherwise search bgg_id = pair.get("bgg_id") if bgg_id: thing = bgg_thing(bgg_id) else: results = bgg_search(game) if not results: print(f" ⚠️ BGG nicht gefunden für »{game}«") warnings += 1 continue thing = bgg_thing(results[0]["id"]) if not thing: print(f" ⚠️ BGG-Details nicht abrufbar (ID {bgg_id or '?'})") warnings += 1 continue # Verify game name matches (avoid false positives from bad search results) bgg_name = thing["name"].lower() claimed_lower = game.lower() if bgg_name != claimed_lower and claimed_lower not in bgg_name and bgg_name not in claimed_lower: print(f" ⚠️ BGG fand »{thing['name']}« statt »{game}« — Spiel vermutlich zu neu für BGG") warnings += 1 continue designers = [normalize_name(d) for d in thing["designers"]] claimed = normalize_name(pair.get("claimed_author", "")) print(f" BGG: {thing['name']} ({thing['year']})") print(f" Designer: {', '.join(thing['designers']) if thing['designers'] else 'KEINE'}") if claimed and claimed not in " ".join(designers): # Try partial match (last name) claimed_parts = claimed.split() found = False for d in designers: if any(part in d for part in claimed_parts if len(part) > 2): found = True break if not found: print(f" ❌ FAKTENFEHLER: »{pair.get('claimed_author', '?')}« ist NICHT Designer von »{thing['name']}«!") errors += 1 else: print(f" ✅ Teil-Match für »{pair.get('claimed_author', '?')}«") elif claimed: print(f" ✅ Autor bestätigt") else: print(f" ℹ️ Kein expliziter Autor-Anspruch zum Abgleichen") print(f"\n{'='*50}") if errors == 0 and warnings == 0: print("✅ ALLE AUTOREN-ZUORDNUNGEN KORREKT") sys.exit(0) elif errors == 0: print(f"⚠️ {warnings} Warnung(en) — KEINE Faktenfehler") sys.exit(0) else: print(f"❌ {errors} FAKTENFEHLER in Autoren-Zuordnungen!") sys.exit(1) if __name__ == "__main__": main()