From 199f8b080d217daa2f35bd15777ea14e3e6bd10a Mon Sep 17 00:00:00 2001 From: Flo Hartmann Date: Tue, 25 Aug 2026 00:44:31 +0000 Subject: [PATCH] Cliquenabend: Release-Feld der Spieldetailseiten parsen (echtes Erscheinungsjahr statt Update-Datum) --- plugins/neuheiten/quellen/cliquenabend.py | 52 ++++++++++++++++++++++- tests/test_neuheiten_quellen.py | 38 +++++++++++++++-- 2 files changed, 85 insertions(+), 5 deletions(-) diff --git a/plugins/neuheiten/quellen/cliquenabend.py b/plugins/neuheiten/quellen/cliquenabend.py index c40cdda..f6ef03f 100644 --- a/plugins/neuheiten/quellen/cliquenabend.py +++ b/plugins/neuheiten/quellen/cliquenabend.py @@ -10,12 +10,17 @@ Die Seite ist nicht paginiert. """ from __future__ import annotations +import logging import re +from urllib.parse import urljoin from bs4 import BeautifulSoup +from bs4.element import Tag from .basis import QuellenAdapter, QuellenTreffer, bild_aus_element +_logger = logging.getLogger(__name__) + DATUM_MUSTER = re.compile(r"\d{2}\.\d{2}\.(\d{2})\b") BASIS_URL = "https://www.cliquenabend.de" @@ -40,6 +45,42 @@ class CliquenabendQuelle(QuellenAdapter): treffer.append(self._zeile(zellen, url)) return [t for t in treffer if t is not None], [] + def _release_jahr(self, spiel_url: str) -> str | None: + """Liest das Release-Feld von der Spieldetailseite. + + Die Detailseite (…/spiele/….html) enthält ein Feld + `Release:
JAHR
` — meist nur das Jahr (z. B. + „2025“), gelegentlich ein genaues Datum. Liefert die rohe Angabe + zurück; `jahr_aus_datumsangabe` leitet daraus später das Jahr ab. + Fehler (404, Strukturänderung) führen zu None und blockieren nie. + """ + try: + antwort = self.client.hole(spiel_url) + except Exception: + _logger.warning("Cliquenabend: Detailseite nicht erreichbar (%s)", spiel_url) + return None + if antwort.status_code != 200: + return None + suppe = BeautifulSoup(antwort.content, "html.parser") + fett = suppe.find( + "b", string=lambda s: isinstance(s, str) and s.strip() == "Release:" + ) + if not isinstance(fett, Tag): + return None + # Wert steht direkt hinter dem Label (
-getrennt); der nächste + # String-Knoten nach dem Label-Element (nicht darin) ist der Wert. + text = fett.next_sibling + while isinstance(text, Tag): + text = text.next_sibling + while isinstance(text, str) and not text.strip(): + text = text.next_sibling + while isinstance(text, Tag): + text = text.next_sibling + if not isinstance(text, str): + return None + wert = text.strip() + return wert or None + def _zeile(self, zellen, url: str) -> QuellenTreffer | None: spiel_zelle = zellen[3] titel_link = spiel_zelle.find("a", href=re.compile(r"^/spiele/")) @@ -50,8 +91,6 @@ class CliquenabendQuelle(QuellenAdapter): titel = (fett or titel_link).get_text(" ", strip=True) href = titel_link.get("href") if href: - from urllib.parse import urljoin - quellen_url = urljoin(BASIS_URL, href) if not titel: return None @@ -66,6 +105,15 @@ class CliquenabendQuelle(QuellenAdapter): # Datum so übernehmen, wie es in der Quelle steht (z. B. „15.10.25“). datum = fund.group(0) + # Release-Feld der Detailseite hat Vorrang: dort steht das echte + # Erscheinungsjahr/-datum statt des Update-Datums der Mese-Tabelle. + if titel_link is not None: + href = titel_link.get("href") + if href: + release = self._release_jahr(urljoin(BASIS_URL, href)) + if release: + datum = release + return QuellenTreffer( titel=titel, verlag=verlag, diff --git a/tests/test_neuheiten_quellen.py b/tests/test_neuheiten_quellen.py index 18a9635..f576a89 100644 --- a/tests/test_neuheiten_quellen.py +++ b/tests/test_neuheiten_quellen.py @@ -124,10 +124,21 @@ CLIQUENABEND_SEITE = """ 26.09.25Piatnik Profil ,6-G200
Elixirus
Dickie Chapin - 01.10.25ohne Link—Nur TexttitelUnbekannt + 01.10.25ohne Link—Nur TexttitelUnbekannt """ +CLIQUENABEND_DETAIL_1 = """ + + Release:
+ 2025
+ Anzahl der Spieler:
+ 1 bis 4 Spieler
+ +""" + +CLIQUENABEND_DETAIL_FEHLT = "

Seite ohne Release-Feld

" + class FakeUhr: """Steuerbare Monoton-Uhr.""" @@ -306,7 +317,11 @@ def test_parser_spielessen_nutzt_json_api_endpunkt(): def test_parser_cliquenabend_liest_tabellenspalten(): adapter, _ = _adapter( CliquenabendQuelle, - {"https://www.cliquenabend.de/events/130000-SPIEL-2025-Essen.html": CLIQUENABEND_SEITE}, + { + "https://www.cliquenabend.de/events/130000-SPIEL-2025-Essen.html": CLIQUENABEND_SEITE, + "https://www.cliquenabend.de/spiele/790610-Galactic-Cruise.html": CLIQUENABEND_DETAIL_1, + "https://www.cliquenabend.de/spiele/790700-Elixirus.html": CLIQUENABEND_DETAIL_FEHLT, + }, ) ergebnis = adapter.sammle() @@ -314,9 +329,26 @@ def test_parser_cliquenabend_liest_tabellenspalten(): erster = ergebnis.treffer[0] assert erster.verlag == "Dranda Games, PD Verlag" assert erster.autor == "T.K. King und Koltin Thompson" - assert erster.erscheinungsdatum_oder_quartal == "15.10.25" + # Release-Feld der Detailseite schlägt das Update-Datum der Tabelle: + assert erster.erscheinungsdatum_oder_quartal == "2025" assert erster.quellen_url == "https://www.cliquenabend.de/spiele/790610-Galactic-Cruise.html" assert ergebnis.treffer[1].verlag == "Piatnik" + # Ohne Release-Feld auf der Detailseite fällt der Parser auf das + # Update-Datum zurück: + assert ergebnis.treffer[1].erscheinungsdatum_oder_quartal == "26.09.25" + + +def test_parser_cliquenabend_detailseite_fehler_blockiert_nicht(): + adapter, _ = _adapter( + CliquenabendQuelle, + { + "https://www.cliquenabend.de/events/130000-SPIEL-2025-Essen.html": CLIQUENABEND_SEITE, + # Detailseiten nicht erreichbar → Update-Datum bleibt erhalten. + }, + ) + ergebnis = adapter.sammle() + assert len(ergebnis.treffer) == 2 + assert ergebnis.treffer[0].erscheinungsdatum_oder_quartal == "15.10.25" def test_parser_cliquenabend_toleriert_fehlende_tabelle():