Fix: dedup BGG-Client nutzt Token; Sync-Filter Jahr+deutscher Titel; Defaults für deutsche Neuerscheinungen
This commit is contained in:
@@ -192,7 +192,8 @@ class DedupPlugin(BasePlugin):
|
||||
if not self._bgg_aktiv or self.context is None:
|
||||
return None
|
||||
if self._bgg_client is None:
|
||||
self._bgg_client = BggPruefClient()
|
||||
token = (os.environ.get("SPIELE_BGG_TOKEN") or "").strip() or None
|
||||
self._bgg_client = BggPruefClient(token=token)
|
||||
return self._bgg_client
|
||||
|
||||
def _protokolliere(self, ergebnis: PruefErgebnis, user: User | None) -> None:
|
||||
|
||||
@@ -33,6 +33,7 @@ class BggPruefClient:
|
||||
self,
|
||||
mindestabstand_sekunden: float = 1.0,
|
||||
*,
|
||||
token: str | None = None,
|
||||
transport: httpx.BaseTransport | None = None,
|
||||
uhr: Callable[[], float] = time.monotonic,
|
||||
schlafen: Callable[[float], None] = time.sleep,
|
||||
@@ -42,8 +43,11 @@ class BggPruefClient:
|
||||
self._zuletzt: float | None = None
|
||||
self._uhr = uhr
|
||||
self._schlafen = schlafen
|
||||
headers: dict[str, str] = {}
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
self._client = httpx.Client(
|
||||
base_url=BASIS_URL, timeout=timeout, transport=transport
|
||||
base_url=BASIS_URL, timeout=timeout, transport=transport, headers=headers
|
||||
)
|
||||
|
||||
def schliessen(self) -> None:
|
||||
|
||||
@@ -87,6 +87,8 @@ class NeuheitenPlugin(BasePlugin):
|
||||
self._quellen_sperre = threading.Lock()
|
||||
self._suchbegriffe: list[str] = []
|
||||
self._max_treffer_pro_suche = 25
|
||||
self._jahr_filter = True
|
||||
self._deutscher_titel_filter = True
|
||||
|
||||
@self.router.get("/neuheiten")
|
||||
def seite(
|
||||
@@ -396,13 +398,23 @@ class NeuheitenPlugin(BasePlugin):
|
||||
super().on_load(context)
|
||||
import os
|
||||
|
||||
self._suchbegriffe = _umgebung_liste("SPIELE_BGG_SUCHBEGRIFFE", "brettspiel")
|
||||
self._suchbegriffe = _umgebung_liste(
|
||||
"SPIELE_BGG_SUCHBEGRIFFE",
|
||||
"brettspiel, gesellschaftsspiel, familienspiel, strategiespiel, "
|
||||
"kartenspiel, würfelspiel",
|
||||
)
|
||||
try:
|
||||
self._max_treffer_pro_suche = int(
|
||||
os.environ.get("SPIELE_BGG_MAX_TREFFER_PRO_SUCHE", "25")
|
||||
os.environ.get("SPIELE_BGG_MAX_TREFFER_PRO_SUCHE", "50")
|
||||
)
|
||||
except ValueError:
|
||||
self._max_treffer_pro_suche = 25
|
||||
self._max_treffer_pro_suche = 50
|
||||
self._jahr_filter = (
|
||||
os.environ.get("SPIELE_BGG_JAHR_FILTER", "1").strip() != "0"
|
||||
)
|
||||
self._deutscher_titel_filter = (
|
||||
os.environ.get("SPIELE_BGG_DEUTSCHE_TITEL_FILTER", "1").strip() != "0"
|
||||
)
|
||||
|
||||
if os.environ.get("SPIELE_BGG_SYNC_AKTIV", "1").strip() == "1":
|
||||
self._scheduler_starten(os.environ.get("SPIELE_BGG_SYNC_INTERVALL_STUNDEN"))
|
||||
@@ -445,6 +457,8 @@ class NeuheitenPlugin(BasePlugin):
|
||||
self.context.session_factory,
|
||||
client,
|
||||
max_treffer_pro_suchbegriff=self._max_treffer_pro_suche,
|
||||
jahr_filter=self._jahr_filter,
|
||||
deutscher_titel_filter=self._deutscher_titel_filter,
|
||||
)
|
||||
return service.synchronisiere(suchbegriffe)
|
||||
finally:
|
||||
|
||||
@@ -3,16 +3,19 @@
|
||||
- Update statt Duplikat: bestehende Einträge werden über die eindeutige
|
||||
BGG-ID gefunden und aktualisiert (Status bleibt erhalten).
|
||||
- Erweiterungen werden im Client und hier auf Elementebene gefiltert.
|
||||
- Prototypen: Die BGG-XML-API2 liefert keinen verlässlichen Prototyp-Marker;
|
||||
als Heuristik werden Titel mit Prototyp-Schlüsselwörtern ausgeschlossen
|
||||
(dokumentierte Einschränkung, siehe README).
|
||||
- Prototypen: Titel mit Prototyp-Schlüsselwörtern werden ausgeschlossen.
|
||||
- Jahresfilter: optional nur Spiele mit Erscheinungsjahr ≥ aktuelles Jahr.
|
||||
- Deutsch-Filter: optional nur Spiele mit deutsch klingendem Titel.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import unicodedata
|
||||
from collections.abc import Iterable, Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import date
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
@@ -28,12 +31,58 @@ PROTOTYP_MUSTER = re.compile(
|
||||
|
||||
THING_BATCH_GROESSE = 20
|
||||
|
||||
#: Funktionswörter, die stark auf einen deutschen Spieltitel hindeuten.
|
||||
_DEUTSCHE_INDIKATOREN = frozenset({
|
||||
"der", "die", "das", "den", "dem", "des",
|
||||
"ein", "eine", "einen", "einem", "einer", "eines",
|
||||
"und", "für", "von", "vom", "zu", "zum", "zur",
|
||||
"im", "am", "beim", "auf", "mit", "aus",
|
||||
})
|
||||
#: Typische Begriffe deutscher Spieltitel (+2 Punkte je Treffer).
|
||||
_DEUTSCHE_SPIELWOERTER = frozenset({
|
||||
"abenteuer", "berg", "blitz", "burg", "dorf", "drache", "drachen",
|
||||
"erde", "feuer", "gärten", "haus", "himmel", "insel", "jagd",
|
||||
"kaiser", "könig", "könige", "königreich", "land", "länder",
|
||||
"meister", "mond", "norden", "osten", "pfad", "reich", "ritter",
|
||||
"schiff", "schloss", "see", "spiel", "spiele", "spielen",
|
||||
"stadt", "stern", "süden", "turm", "wald", "weg", "welt",
|
||||
"westen", "wiese", "wilder", "wüste", "zauber", "zeit",
|
||||
})
|
||||
#: Wörter, die auf einen englischen Titel hindeuten (−1 Punkt je Treffer).
|
||||
_ENGLISCHE_INDIKATOREN = frozenset({
|
||||
"the", "of", "and", "game", "games", "card", "cards", "dice", "board",
|
||||
})
|
||||
_UM_LAUTE = frozenset("äöüßÄÖÜ")
|
||||
_WORT_MUSTER = re.compile(r"[^\W\d_]+", re.UNICODE)
|
||||
|
||||
|
||||
def ist_deutscher_titel(titel: str) -> bool:
|
||||
"""Heuristik: > 0 spricht für einen deutschen Titel.
|
||||
|
||||
Umlaute/ß zählen am stärksten (+3), deutsche Funktionswörter +1,
|
||||
typische deutsche Spielbegriffe +2, englische Indikatoren −1.
|
||||
"""
|
||||
if not titel:
|
||||
return False
|
||||
score = 0
|
||||
if any(zeichen in _UM_LAUTE for zeichen in titel):
|
||||
score += 3
|
||||
woerter = {wort.casefold() for wort in _WORT_MUSTER.findall(titel)}
|
||||
score += sum(1 for wort in woerter if wort in _DEUTSCHE_INDIKATOREN)
|
||||
score += sum(2 for wort in woerter if wort in _DEUTSCHE_SPIELWOERTER)
|
||||
score -= sum(1 for wort in woerter if wort in _ENGLISCHE_INDIKATOREN)
|
||||
return score > 0
|
||||
|
||||
|
||||
def ist_moeglicher_prototyp(titel: str) -> bool:
|
||||
"""Heuristik: Titel klingt nach Prototyp/Protoversion."""
|
||||
return bool(PROTOTYP_MUSTER.search(titel))
|
||||
|
||||
|
||||
def aktuelles_jahr() -> int:
|
||||
return date.today().year
|
||||
|
||||
|
||||
@dataclass
|
||||
class SyncErgebnis:
|
||||
"""Zusammenfassung eines Sync-Laufs."""
|
||||
@@ -51,7 +100,7 @@ class SyncErgebnis:
|
||||
return self.abbruch
|
||||
text = (
|
||||
f"{self.neu} neu, {self.aktualisiert} aktualisiert, "
|
||||
f"{self.gefiltert} gefiltert (Erweiterungen/Prototypen)"
|
||||
f"{self.gefiltert} gefiltert (Erweiterungen/Prototypen/Jahr/Sprache)"
|
||||
)
|
||||
if self.fehler:
|
||||
text += f"; {len(self.fehler)} Fehler"
|
||||
@@ -96,11 +145,16 @@ class SyncService:
|
||||
*,
|
||||
thing_batch_groesse: int = THING_BATCH_GROESSE,
|
||||
max_treffer_pro_suchbegriff: int | None = None,
|
||||
jahr_filter: bool = True,
|
||||
deutscher_titel_filter: bool = True,
|
||||
) -> None:
|
||||
self.session_factory = session_factory
|
||||
self.client = client
|
||||
self.thing_batch_groesse = thing_batch_groesse
|
||||
self.max_treffer_pro_suchbegriff = max_treffer_pro_suchbegriff
|
||||
self.jahr_filter = jahr_filter
|
||||
self.deutscher_titel_filter = deutscher_titel_filter
|
||||
self._min_jahr = aktuelles_jahr()
|
||||
|
||||
def synchronisiere(self, suchbegriffe: Iterable[str]) -> SyncErgebnis:
|
||||
ergebnis = SyncErgebnis()
|
||||
@@ -139,6 +193,13 @@ class SyncService:
|
||||
if ist_moeglicher_prototyp(spiel.titel):
|
||||
ergebnis.gefiltert += 1
|
||||
continue
|
||||
if self.jahr_filter and spiel.erscheinungsjahr is not None:
|
||||
if spiel.erscheinungsjahr < self._min_jahr:
|
||||
ergebnis.gefiltert += 1
|
||||
continue
|
||||
if self.deutscher_titel_filter and not ist_deutscher_titel(spiel.titel):
|
||||
ergebnis.gefiltert += 1
|
||||
continue
|
||||
if speichere_oder_aktualisiere(db, spiel):
|
||||
ergebnis.neu += 1
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user