Fix: dedup BGG-Client nutzt Token; Sync-Filter Jahr+deutscher Titel; Defaults für deutsche Neuerscheinungen
This commit is contained in:
6
.env
6
.env
@@ -1,3 +1,9 @@
|
|||||||
SPIELE_SESSION_SECRET=48ebf7ddae261a931652b5a55cff708aa21cf4936a8bdeb287eeac7cec4ee3e2
|
SPIELE_SESSION_SECRET=48ebf7ddae261a931652b5a55cff708aa21cf4936a8bdeb287eeac7cec4ee3e2
|
||||||
SPIELE_INITIAL_ADMIN_PASSWORD=3fb93f84014ff45406b36ec3
|
SPIELE_INITIAL_ADMIN_PASSWORD=3fb93f84014ff45406b36ec3
|
||||||
SPIELE_BGG_TOKEN=6fa6f9f6-32fd-443d-a5c3-ac2527f6a6fc
|
SPIELE_BGG_TOKEN=6fa6f9f6-32fd-443d-a5c3-ac2527f6a6fc
|
||||||
|
|
||||||
|
# BGG-Sync: deutsche Neuerscheinungen ab aktuellem Jahr
|
||||||
|
SPIELE_BGG_SUCHBEGRIFFE=brettspiel,gesellschaftsspiel,familienspiel,strategiespiel,kartenspiel,würfelspiel
|
||||||
|
SPIELE_BGG_MAX_TREFFER_PRO_SUCHE=50
|
||||||
|
SPIELE_BGG_JAHR_FILTER=1
|
||||||
|
SPIELE_BGG_DEUTSCHE_TITEL_FILTER=1
|
||||||
|
|||||||
@@ -55,9 +55,11 @@ Beim ersten Start wird automatisch ein Admin-Konto angelegt:
|
|||||||
| `SPIELE_TELEGRAM_BOT_TOKEN` | *(leer)* | Bot-Token für den Telegram-Kanal; leer = Dev-Fallback (nur Protokoll) |
|
| `SPIELE_TELEGRAM_BOT_TOKEN` | *(leer)* | Bot-Token für den Telegram-Kanal; leer = Dev-Fallback (nur Protokoll) |
|
||||||
| `SPIELE_BGG_SYNC_AKTIV` | `1` | Hintergrund-Sync des Neuheiten-Plugins an (`1`) oder aus (`0`) |
|
| `SPIELE_BGG_SYNC_AKTIV` | `1` | Hintergrund-Sync des Neuheiten-Plugins an (`1`) oder aus (`0`) |
|
||||||
| `SPIELE_BGG_SYNC_INTERVALL_STUNDEN` | `24` | Intervall des BGG-Syncs in Stunden (min. 1) |
|
| `SPIELE_BGG_SYNC_INTERVALL_STUNDEN` | `24` | Intervall des BGG-Syncs in Stunden (min. 1) |
|
||||||
| `SPIELE_BGG_SUCHBEGRIFFE` | `brettspiel` | Komma-getrennte Suchbegriffe für den regelmäßigen Sync |
|
| `SPIELE_BGG_SUCHBEGRIFFE` | `brettspiel, gesellschaftsspiel, familienspiel, strategiespiel, kartenspiel, würfelspiel` | Komma-getrennte Suchbegriffe für den regelmäßigen Sync (deutsche Spielbegriffe) |
|
||||||
| `SPIELE_BGG_MAX_TREFFER_PRO_SUCHE` | `25` | Obergrenze Treffer je Suchbegriff (schont das BGG-Rate-Limit) |
|
| `SPIELE_BGG_MAX_TREFFER_PRO_SUCHE` | `50` | Obergrenze Treffer je Suchbegriff (schont das BGG-Rate-Limit) |
|
||||||
| `SPIELE_BGG_TOKEN` | *(leer)* | API-Token für die BGG-XML-API2, wird als `Authorization: Bearer …`-Header gesendet; seit der Token-Pflicht von BGG erforderlich (siehe [BGG-Thread 3602374](https://boardgamegeek.com/thread/3602374)) — ohne Token wird der Sync übersprungen |
|
| `SPIELE_BGG_JAHR_FILTER` | `1` | Nur Spiele mit Erscheinungsjahr ≥ aktuelles Jahr übernehmen (`1`) oder alle (`0`) |
|
||||||
|
| `SPIELE_BGG_DEUTSCHE_TITEL_FILTER` | `1` | Nur Spiele mit deutsch klingendem Titel übernehmen (`1`) oder alle (`0`) |
|
||||||
|
| `SPIELE_BGG_TOKEN` | *(leer)* | API-Token für die BGG-XML-API2, wird als `Authorization: Bearer *** gesendet; seit der Token-Pflicht von BGG erforderlich (siehe [BGG-Thread 3602374](https://boardgamegeek.com/thread/3602374)) — ohne Token wird der Sync übersprungen |
|
||||||
| `SPIELE_DEDUP_BGG_AKTIV` | `1` | BGG-Zusatzdaten für die Dedup-Prüfung an (`1`) oder aus (`0`): Alternate-Names und Erweiterungs-Relationen |
|
| `SPIELE_DEDUP_BGG_AKTIV` | `1` | BGG-Zusatzdaten für die Dedup-Prüfung an (`1`) oder aus (`0`): Alternate-Names und Erweiterungs-Relationen |
|
||||||
| `SPIELE_DEDUP_LLM_AKTIV` | `0` | LLM-Zweitprüfung der Dedup-Grenzfälle an (`1`) oder aus (`0`) |
|
| `SPIELE_DEDUP_LLM_AKTIV` | `0` | LLM-Zweitprüfung der Dedup-Grenzfälle an (`1`) oder aus (`0`) |
|
||||||
| `SPIELE_DEDUP_LLM_BASIS_URL` | *(leer)* | Basis-URL einer OpenAI-kompatiblen Chat-Completions-API, z. B. `https://api.openai.com/v1` |
|
| `SPIELE_DEDUP_LLM_BASIS_URL` | *(leer)* | Basis-URL einer OpenAI-kompatiblen Chat-Completions-API, z. B. `https://api.openai.com/v1` |
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ services:
|
|||||||
- SPIELE_SESSION_SECRET=${SPIELE_SESSION_SECRET:-change-me-in-production}
|
- SPIELE_SESSION_SECRET=${SPIELE_SESSION_SECRET:-change-me-in-production}
|
||||||
# Vor dem ersten Start setzen, sonst wird der Admin mit "admin" angelegt:
|
# Vor dem ersten Start setzen, sonst wird der Admin mit "admin" angelegt:
|
||||||
- SPIELE_INITIAL_ADMIN_PASSWORD=${SPIELE_INITIAL_ADMIN_PASSWORD:-admin}
|
- SPIELE_INITIAL_ADMIN_PASSWORD=${SPIELE_INITIAL_ADMIN_PASSWORD:-admin}
|
||||||
|
# BGG XML API2 (Pflicht seit Token-Umstellung): siehe .env
|
||||||
|
- SPIELE_BGG_TOKEN=${SPIELE_BGG_TOKEN}
|
||||||
# Benachrichtigungen (optional): ohne SMTP-Host/Bot-Token wird nur protokolliert
|
# Benachrichtigungen (optional): ohne SMTP-Host/Bot-Token wird nur protokolliert
|
||||||
#- SPIELE_SMTP_HOST=smtp.example.org
|
#- SPIELE_SMTP_HOST=smtp.example.org
|
||||||
#- SPIELE_SMTP_PORT=587
|
#- SPIELE_SMTP_PORT=587
|
||||||
|
|||||||
@@ -192,7 +192,8 @@ class DedupPlugin(BasePlugin):
|
|||||||
if not self._bgg_aktiv or self.context is None:
|
if not self._bgg_aktiv or self.context is None:
|
||||||
return None
|
return None
|
||||||
if self._bgg_client is 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
|
return self._bgg_client
|
||||||
|
|
||||||
def _protokolliere(self, ergebnis: PruefErgebnis, user: User | None) -> None:
|
def _protokolliere(self, ergebnis: PruefErgebnis, user: User | None) -> None:
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class BggPruefClient:
|
|||||||
self,
|
self,
|
||||||
mindestabstand_sekunden: float = 1.0,
|
mindestabstand_sekunden: float = 1.0,
|
||||||
*,
|
*,
|
||||||
|
token: str | None = None,
|
||||||
transport: httpx.BaseTransport | None = None,
|
transport: httpx.BaseTransport | None = None,
|
||||||
uhr: Callable[[], float] = time.monotonic,
|
uhr: Callable[[], float] = time.monotonic,
|
||||||
schlafen: Callable[[float], None] = time.sleep,
|
schlafen: Callable[[float], None] = time.sleep,
|
||||||
@@ -42,8 +43,11 @@ class BggPruefClient:
|
|||||||
self._zuletzt: float | None = None
|
self._zuletzt: float | None = None
|
||||||
self._uhr = uhr
|
self._uhr = uhr
|
||||||
self._schlafen = schlafen
|
self._schlafen = schlafen
|
||||||
|
headers: dict[str, str] = {}
|
||||||
|
if token:
|
||||||
|
headers["Authorization"] = f"Bearer {token}"
|
||||||
self._client = httpx.Client(
|
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:
|
def schliessen(self) -> None:
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ class NeuheitenPlugin(BasePlugin):
|
|||||||
self._quellen_sperre = threading.Lock()
|
self._quellen_sperre = threading.Lock()
|
||||||
self._suchbegriffe: list[str] = []
|
self._suchbegriffe: list[str] = []
|
||||||
self._max_treffer_pro_suche = 25
|
self._max_treffer_pro_suche = 25
|
||||||
|
self._jahr_filter = True
|
||||||
|
self._deutscher_titel_filter = True
|
||||||
|
|
||||||
@self.router.get("/neuheiten")
|
@self.router.get("/neuheiten")
|
||||||
def seite(
|
def seite(
|
||||||
@@ -396,13 +398,23 @@ class NeuheitenPlugin(BasePlugin):
|
|||||||
super().on_load(context)
|
super().on_load(context)
|
||||||
import os
|
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:
|
try:
|
||||||
self._max_treffer_pro_suche = int(
|
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:
|
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":
|
if os.environ.get("SPIELE_BGG_SYNC_AKTIV", "1").strip() == "1":
|
||||||
self._scheduler_starten(os.environ.get("SPIELE_BGG_SYNC_INTERVALL_STUNDEN"))
|
self._scheduler_starten(os.environ.get("SPIELE_BGG_SYNC_INTERVALL_STUNDEN"))
|
||||||
@@ -445,6 +457,8 @@ class NeuheitenPlugin(BasePlugin):
|
|||||||
self.context.session_factory,
|
self.context.session_factory,
|
||||||
client,
|
client,
|
||||||
max_treffer_pro_suchbegriff=self._max_treffer_pro_suche,
|
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)
|
return service.synchronisiere(suchbegriffe)
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -3,16 +3,19 @@
|
|||||||
- Update statt Duplikat: bestehende Einträge werden über die eindeutige
|
- Update statt Duplikat: bestehende Einträge werden über die eindeutige
|
||||||
BGG-ID gefunden und aktualisiert (Status bleibt erhalten).
|
BGG-ID gefunden und aktualisiert (Status bleibt erhalten).
|
||||||
- Erweiterungen werden im Client und hier auf Elementebene gefiltert.
|
- Erweiterungen werden im Client und hier auf Elementebene gefiltert.
|
||||||
- Prototypen: Die BGG-XML-API2 liefert keinen verlässlichen Prototyp-Marker;
|
- Prototypen: Titel mit Prototyp-Schlüsselwörtern werden ausgeschlossen.
|
||||||
als Heuristik werden Titel mit Prototyp-Schlüsselwörtern ausgeschlossen
|
- Jahresfilter: optional nur Spiele mit Erscheinungsjahr ≥ aktuelles Jahr.
|
||||||
(dokumentierte Einschränkung, siehe README).
|
- Deutsch-Filter: optional nur Spiele mit deutsch klingendem Titel.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import unicodedata
|
||||||
from collections.abc import Iterable, Sequence
|
from collections.abc import Iterable, Sequence
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session, sessionmaker
|
from sqlalchemy.orm import Session, sessionmaker
|
||||||
@@ -28,12 +31,58 @@ PROTOTYP_MUSTER = re.compile(
|
|||||||
|
|
||||||
THING_BATCH_GROESSE = 20
|
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:
|
def ist_moeglicher_prototyp(titel: str) -> bool:
|
||||||
"""Heuristik: Titel klingt nach Prototyp/Protoversion."""
|
"""Heuristik: Titel klingt nach Prototyp/Protoversion."""
|
||||||
return bool(PROTOTYP_MUSTER.search(titel))
|
return bool(PROTOTYP_MUSTER.search(titel))
|
||||||
|
|
||||||
|
|
||||||
|
def aktuelles_jahr() -> int:
|
||||||
|
return date.today().year
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SyncErgebnis:
|
class SyncErgebnis:
|
||||||
"""Zusammenfassung eines Sync-Laufs."""
|
"""Zusammenfassung eines Sync-Laufs."""
|
||||||
@@ -51,7 +100,7 @@ class SyncErgebnis:
|
|||||||
return self.abbruch
|
return self.abbruch
|
||||||
text = (
|
text = (
|
||||||
f"{self.neu} neu, {self.aktualisiert} aktualisiert, "
|
f"{self.neu} neu, {self.aktualisiert} aktualisiert, "
|
||||||
f"{self.gefiltert} gefiltert (Erweiterungen/Prototypen)"
|
f"{self.gefiltert} gefiltert (Erweiterungen/Prototypen/Jahr/Sprache)"
|
||||||
)
|
)
|
||||||
if self.fehler:
|
if self.fehler:
|
||||||
text += f"; {len(self.fehler)} Fehler"
|
text += f"; {len(self.fehler)} Fehler"
|
||||||
@@ -96,11 +145,16 @@ class SyncService:
|
|||||||
*,
|
*,
|
||||||
thing_batch_groesse: int = THING_BATCH_GROESSE,
|
thing_batch_groesse: int = THING_BATCH_GROESSE,
|
||||||
max_treffer_pro_suchbegriff: int | None = None,
|
max_treffer_pro_suchbegriff: int | None = None,
|
||||||
|
jahr_filter: bool = True,
|
||||||
|
deutscher_titel_filter: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.session_factory = session_factory
|
self.session_factory = session_factory
|
||||||
self.client = client
|
self.client = client
|
||||||
self.thing_batch_groesse = thing_batch_groesse
|
self.thing_batch_groesse = thing_batch_groesse
|
||||||
self.max_treffer_pro_suchbegriff = max_treffer_pro_suchbegriff
|
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:
|
def synchronisiere(self, suchbegriffe: Iterable[str]) -> SyncErgebnis:
|
||||||
ergebnis = SyncErgebnis()
|
ergebnis = SyncErgebnis()
|
||||||
@@ -139,6 +193,13 @@ class SyncService:
|
|||||||
if ist_moeglicher_prototyp(spiel.titel):
|
if ist_moeglicher_prototyp(spiel.titel):
|
||||||
ergebnis.gefiltert += 1
|
ergebnis.gefiltert += 1
|
||||||
continue
|
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):
|
if speichere_oder_aktualisiere(db, spiel):
|
||||||
ergebnis.neu += 1
|
ergebnis.neu += 1
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -389,3 +389,25 @@ def test_bgg_pruefclient_toleroert_fehler():
|
|||||||
assert client.vorgaenger(99) == []
|
assert client.vorgaenger(99) == []
|
||||||
finally:
|
finally:
|
||||||
client.schliessen()
|
client.schliessen()
|
||||||
|
|
||||||
|
|
||||||
|
def test_bgg_pruefclient_sendet_token_header():
|
||||||
|
from plugins.dedup.bgg import BggPruefClient
|
||||||
|
|
||||||
|
gesehen: list[httpx.Request] = []
|
||||||
|
|
||||||
|
def handler(request: httpx.Request) -> httpx.Response:
|
||||||
|
gesehen.append(request)
|
||||||
|
return httpx.Response(200, content=THING_XML.encode())
|
||||||
|
|
||||||
|
client = BggPruefClient(
|
||||||
|
mindestabstand_sekunden=0.0,
|
||||||
|
token="mein-token",
|
||||||
|
transport=httpx.MockTransport(handler),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
assert client.alternativen(99) == ["Catan Expansion", "Siedler-Erweiterung"]
|
||||||
|
finally:
|
||||||
|
client.schliessen()
|
||||||
|
assert gesehen
|
||||||
|
assert gesehen[0].headers["Authorization"] == "Bearer mein-token"
|
||||||
|
|||||||
@@ -94,7 +94,9 @@ def test_sync_legt_eintraege_an(session_factory):
|
|||||||
suchergebnisse={"catan": [SuchTreffer(13, "Catan", 1995)]},
|
suchergebnisse={"catan": [SuchTreffer(13, "Catan", 1995)]},
|
||||||
details_pro_id={13: _spiel()},
|
details_pro_id={13: _spiel()},
|
||||||
)
|
)
|
||||||
service = SyncService(session_factory, client)
|
service = SyncService(
|
||||||
|
session_factory, client, jahr_filter=False, deutscher_titel_filter=False
|
||||||
|
)
|
||||||
|
|
||||||
ergebnis = service.synchronisiere(["catan"])
|
ergebnis = service.synchronisiere(["catan"])
|
||||||
|
|
||||||
@@ -117,7 +119,9 @@ def test_sync_aktualisiert_bestehende_eintraege_statt_duplikate(session_factory)
|
|||||||
suchergebnisse={"catan": [SuchTreffer(13, "Catan", 1995)]},
|
suchergebnisse={"catan": [SuchTreffer(13, "Catan", 1995)]},
|
||||||
details_pro_id={13: _spiel()},
|
details_pro_id={13: _spiel()},
|
||||||
)
|
)
|
||||||
service = SyncService(session_factory, client)
|
service = SyncService(
|
||||||
|
session_factory, client, jahr_filter=False, deutscher_titel_filter=False
|
||||||
|
)
|
||||||
|
|
||||||
service.synchronisiere(["catan"])
|
service.synchronisiere(["catan"])
|
||||||
|
|
||||||
@@ -142,7 +146,9 @@ def test_sync_veraenderter_titel_gleiche_bgg_id_kein_duplikat(session_factory):
|
|||||||
suchergebnisse={"catan": [SuchTreffer(13, "Catan", 1995)]},
|
suchergebnisse={"catan": [SuchTreffer(13, "Catan", 1995)]},
|
||||||
details_pro_id={13: _spiel()},
|
details_pro_id={13: _spiel()},
|
||||||
)
|
)
|
||||||
service = SyncService(session_factory, client)
|
service = SyncService(
|
||||||
|
session_factory, client, jahr_filter=False, deutscher_titel_filter=False
|
||||||
|
)
|
||||||
service.synchronisiere(["catan"])
|
service.synchronisiere(["catan"])
|
||||||
|
|
||||||
client.details_pro_id[13] = _spiel(titel="Catan (Neuausgabe)")
|
client.details_pro_id[13] = _spiel(titel="Catan (Neuausgabe)")
|
||||||
@@ -176,7 +182,9 @@ def test_sync_filtert_erweiterungen(session_factory):
|
|||||||
55: _spiel(55, "Catan: Erweiterung", typ="boardgameexpansion"),
|
55: _spiel(55, "Catan: Erweiterung", typ="boardgameexpansion"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
service = SyncService(session_factory, client)
|
service = SyncService(
|
||||||
|
session_factory, client, jahr_filter=False, deutscher_titel_filter=False
|
||||||
|
)
|
||||||
|
|
||||||
ergebnis = service.synchronisiere(["catan"])
|
ergebnis = service.synchronisiere(["catan"])
|
||||||
|
|
||||||
@@ -200,7 +208,9 @@ def test_sync_filtert_prototypen(session_factory):
|
|||||||
78: _spiel(78, "My Prototype Game"),
|
78: _spiel(78, "My Prototype Game"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
service = SyncService(session_factory, client)
|
service = SyncService(
|
||||||
|
session_factory, client, jahr_filter=False, deutscher_titel_filter=False
|
||||||
|
)
|
||||||
|
|
||||||
ergebnis = service.synchronisiere(["essen"])
|
ergebnis = service.synchronisiere(["essen"])
|
||||||
|
|
||||||
@@ -216,6 +226,61 @@ def test_prototyp_heuristik():
|
|||||||
assert not ist_moeglicher_prototyp("Die Burgen von Burgund")
|
assert not ist_moeglicher_prototyp("Die Burgen von Burgund")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Filter: Jahr und deutscher Titel ----------
|
||||||
|
|
||||||
|
|
||||||
|
def test_sync_filtert_nach_jahr_und_deutschem_titel(session_factory):
|
||||||
|
aktuelles = 2026
|
||||||
|
client = FakeBggClient(
|
||||||
|
suchergebnisse={
|
||||||
|
"mix": [
|
||||||
|
SuchTreffer(13, "Catan", aktuelles - 1),
|
||||||
|
SuchTreffer(42, "Die neuen Siedler", aktuelles),
|
||||||
|
SuchTreffer(99, "Future Game", aktuelles + 1),
|
||||||
|
SuchTreffer(88, "The Old English Game", aktuelles),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
details_pro_id={
|
||||||
|
13: _spiel(13, "Catan", erscheinungsjahr=aktuelles - 1),
|
||||||
|
42: _spiel(42, "Die neuen Siedler", erscheinungsjahr=aktuelles),
|
||||||
|
99: _spiel(99, "Future Game", erscheinungsjahr=aktuelles + 1),
|
||||||
|
88: _spiel(88, "The Old English Game", erscheinungsjahr=aktuelles),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
service = SyncService(session_factory, client)
|
||||||
|
service._min_jahr = aktuelles
|
||||||
|
|
||||||
|
ergebnis = service.synchronisiere(["mix"])
|
||||||
|
|
||||||
|
assert ergebnis.neu == 1 # nur "Die neuen Siedler" überlebt beide Filter
|
||||||
|
assert ergebnis.gefiltert == 3 # 1 zu alt + 1 zukünftig aber englisch + 1 aktuell aber englisch
|
||||||
|
eintraege = _alle_eintraege(session_factory)
|
||||||
|
assert [e.bgg_id for e in eintraege] == [42]
|
||||||
|
|
||||||
|
|
||||||
|
def test_sync_ohne_filter_behält_alle_jahre_und_sprachen(session_factory):
|
||||||
|
client = FakeBggClient(
|
||||||
|
suchergebnisse={
|
||||||
|
"mix": [
|
||||||
|
SuchTreffer(13, "Catan", 1995),
|
||||||
|
SuchTreffer(88, "The English Game", 2026),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
details_pro_id={
|
||||||
|
13: _spiel(13, "Catan", erscheinungsjahr=1995),
|
||||||
|
88: _spiel(88, "The English Game", erscheinungsjahr=2026),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
service = SyncService(
|
||||||
|
session_factory, client, jahr_filter=False, deutscher_titel_filter=False
|
||||||
|
)
|
||||||
|
|
||||||
|
ergebnis = service.synchronisiere(["mix"])
|
||||||
|
|
||||||
|
assert ergebnis.neu == 2
|
||||||
|
assert ergebnis.gefiltert == 0
|
||||||
|
|
||||||
|
|
||||||
# ---------- Robustheit ----------
|
# ---------- Robustheit ----------
|
||||||
|
|
||||||
|
|
||||||
@@ -233,7 +298,9 @@ def test_401_bricht_sync_ab_mit_klarer_meldung(session_factory):
|
|||||||
suchergebnisse={"gut": [SuchTreffer(13, "Catan", 1995)]},
|
suchergebnisse={"gut": [SuchTreffer(13, "Catan", 1995)]},
|
||||||
details_pro_id={13: _spiel()},
|
details_pro_id={13: _spiel()},
|
||||||
)
|
)
|
||||||
service = SyncService(session_factory, client)
|
service = SyncService(
|
||||||
|
session_factory, client, jahr_filter=False, deutscher_titel_filter=False
|
||||||
|
)
|
||||||
|
|
||||||
ergebnis = service.synchronisiere(["gut", "abgelehnt", "dritter"])
|
ergebnis = service.synchronisiere(["gut", "abgelehnt", "dritter"])
|
||||||
|
|
||||||
@@ -255,7 +322,9 @@ def test_fehler_bei_einem_suchbegriff_bricht_lauf_nicht_ab(session_factory):
|
|||||||
suchergebnisse={"gut": [SuchTreffer(13, "Catan", 1995)]},
|
suchergebnisse={"gut": [SuchTreffer(13, "Catan", 1995)]},
|
||||||
details_pro_id={13: _spiel()},
|
details_pro_id={13: _spiel()},
|
||||||
)
|
)
|
||||||
service = SyncService(session_factory, client)
|
service = SyncService(
|
||||||
|
session_factory, client, jahr_filter=False, deutscher_titel_filter=False
|
||||||
|
)
|
||||||
|
|
||||||
ergebnis = service.synchronisiere(["kaputt", "gut"])
|
ergebnis = service.synchronisiere(["kaputt", "gut"])
|
||||||
|
|
||||||
|
|||||||
@@ -194,6 +194,8 @@ def test_sync_endpunkt_rollen_pruefung(app, settings):
|
|||||||
def test_manueller_sync_legt_eintraege_an(monkeypatch, settings):
|
def test_manueller_sync_legt_eintraege_an(monkeypatch, settings):
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
monkeypatch.setenv("SPIELE_BGG_JAHR_FILTER", "0")
|
||||||
|
monkeypatch.setenv("SPIELE_BGG_DEUTSCHE_TITEL_FILTER", "0")
|
||||||
fake = FakeBggClient(
|
fake = FakeBggClient(
|
||||||
treffer=[SuchTreffer(13, "Catan", 1995)],
|
treffer=[SuchTreffer(13, "Catan", 1995)],
|
||||||
spiel_details=[
|
spiel_details=[
|
||||||
|
|||||||
Reference in New Issue
Block a user