Teil 1 — Coverbilder: - Migration 0004_bild_url (neuheiten), 0002_bild_url (planung, archiv mit beiden Spiegel-Tabellen); nullable VARCHAR(500), SQLite↔Postgres portabel, bestehende Zeilen bleiben erhalten - BGG-Client extrahiert Cover aus der Thing-Antwort (bevorzugt <image>, sonst <thumbnail>; protokoll-relative URLs → https) - Sync-Upsert pflegt bild_url beim Update; fehlendes Bild löscht keines - Web-Quellen übernehmen img-URLs aus den Listenelementen (src/data-src, relative Pfade gegen die Seiten-URL aufgelöst); SPIEL-Essen nutzt das BILDER-Feld des JSON-Endpunkts - Verschiebung Neuheiten→Planung und Archivierung/Wiederherstellen schreiben bild_url mit - Anzeige: kleine Vorschaubilder (40x56) in den Listen, groß (max 200px) in Planung-Bearbeiten/Prüf-Dialog; Platzhalter-Div ohne Bild; Hotlinking mit loading=lazy + decoding=async Teil 2 — Responsives Layout: - Basislayout: Burger-Menü (Alpine.js) auf Mobil, Desktop-Leiste ab md: - Listen kollabieren <768px zu Karten-Ansichten (Cover links, Titel + Metadaten rechts, Status-Badge oben); Desktop behält Tabellenform - Touch-Ziele >= 44px auf Mobil, Formulare vollbreit mit Labels oben - Sync-Übersicht/Audit-Log bleiben horizontal scrollbare Tabellen Tests: 15 neue Fälle (tests/test_covers.py) — Migration inkl. Daten-Erhalt, BGG-Parsing gemockt, Quellen-Übernahme, Template-Render; 316 Tests grün. README aktualisiert.
67 lines
2.5 KiB
Python
67 lines
2.5 KiB
Python
"""Adapter für die Brettspielbox-Messevorschau (A-Z-Liste).
|
||
|
||
Seite: …/brettspiel-neuheiten-spiel-2026-herbst-2026-a-z/ (WordPress).
|
||
|
||
Struktur (Stand Analyse August 2026): Die komplette A-Z-Liste steht auf
|
||
einer Seite in `div.wp-block-columns`-Blöcken — pro Buchstabe ein
|
||
Absatz (`p`) mit dem Buchstaben bzw. „0-9“ und eine `ul` mit einem
|
||
`li` pro Spiel im Format „Titel (Verlag)“; optionale Links im Eintrag
|
||
zeigen auf Vorstellungs-/Rezensionsartikel und dienen als quellen_url,
|
||
sonst gilt die Listenseite selbst.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
|
||
from bs4 import BeautifulSoup
|
||
|
||
from .basis import QuellenAdapter, QuellenTreffer, bild_aus_element
|
||
|
||
TITEL_VERLAG_MUSTER = re.compile(r"^\s*(.+?)\s*\(([^()]*)\)\s*$")
|
||
|
||
|
||
class BrettspielboxQuelle(QuellenAdapter):
|
||
name = "brettspielbox"
|
||
anzeigename = "Brettspielbox Messevorschau (A–Z)"
|
||
start_urls = (
|
||
"https://brettspielbox.de/brettspiel-neuheiten-spiel-2026-herbst-2026-a-z/",
|
||
)
|
||
|
||
def seite_verarbeiten(self, inhalt: bytes, url: str):
|
||
suppe = BeautifulSoup(inhalt, "html.parser")
|
||
treffer: list[QuellenTreffer] = []
|
||
for spalte in suppe.select(".wp-block-columns"):
|
||
for liste in spalte.find_all("ul"):
|
||
for eintrag in liste.find_all("li"):
|
||
text = eintrag.get_text(" ", strip=True)
|
||
titel, verlag = _zerlege_eintrag(text)
|
||
if not titel:
|
||
continue
|
||
link = eintrag.find("a", href=True)
|
||
from urllib.parse import urljoin
|
||
|
||
quellen_url = (
|
||
urljoin(url, link["href"]) if link is not None else url
|
||
)
|
||
treffer.append(
|
||
QuellenTreffer(
|
||
titel=titel,
|
||
verlag=verlag,
|
||
autor=None,
|
||
erscheinungsdatum_oder_quartal=None,
|
||
quellen_url=quellen_url,
|
||
bild_url=bild_aus_element(eintrag, url),
|
||
)
|
||
)
|
||
return treffer, []
|
||
|
||
|
||
def _zerlege_eintrag(text: str) -> tuple[str | None, str | None]:
|
||
"""„Titel (Verlag)“ → (Titel, Verlag); ohne Klammer nur Titel."""
|
||
fund = TITEL_VERLAG_MUSTER.match(text)
|
||
if fund is None:
|
||
return (text.strip() or None), None
|
||
verlag = fund.group(2).strip() or None
|
||
titel = fund.group(1).strip()
|
||
return (titel or None), verlag
|