Files
spiele-redaktion/plugins/neuheiten/quellen/spielbox.py
Flo Hartmann 96d6af8163 Coverbilder (bild_url) + responsives Layout für Mobil/Tablet
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.
2026-08-24 16:54:56 +00:00

90 lines
3.2 KiB
Python

"""Adapter für https://www.spielbox.de/neuheiten-neue-spiele (Joomla-Blog).
Seitenstruktur (Stand Analyse August 2026):
- Pro Beitrag ein `div.item` mit Titel-Link in `div.page-header h2 a`,
Beitragsdatum (TT.MM.JJJJ) im ersten Absatz (`p > strong`).
- Pagination über `.pagination`-Links der Form `?start=N` (6 Beiträge je
Seite) — alle Seiten werden durchlaufen, bis keine neue Seite mehr
gefunden wird.
- Verlag: Heuristik — Präfix des Titels vor dem Doppelpunkt
(z. B. „Pegasus Spiele: Next Station Berlin“); fehlt er, bleibt der
Verlag leer. Ein Autor wird auf dieser Liste nicht strukturiert
mitgeliefert.
"""
from __future__ import annotations
import re
from bs4 import BeautifulSoup
from .basis import QuellenAdapter, QuellenTreffer, bild_aus_element
DATUM_MUSTER = re.compile(r"(\d{2}\.\d{2}\.\d{4})")
class SpielboxQuelle(QuellenAdapter):
name = "spielbox"
anzeigename = "spielbox.de Neuheiten"
start_urls = ("https://www.spielbox.de/neuheiten-neue-spiele",)
def seite_verarbeiten(self, inhalt: bytes, url: str):
suppe = BeautifulSoup(inhalt, "html.parser")
treffer: list[QuellenTreffer] = []
for element in suppe.select("div.item"):
kopf_link = element.select_one("div.page-header h2 a") or element.find("h2")
if kopf_link is None:
continue # Strukturänderung: Beitrag überspringen, Lauf hält an
titel = kopf_link.get_text(" ", strip=True)
if not titel:
continue
href = kopf_link.get("href") if hasattr(kopf_link, "get") else None
quellen_url = _absolute_url(href, url) or url
datum = None
for absatz in element.find_all("p"):
strong = absatz.find("strong")
if strong is not None:
fund = DATUM_MUSTER.search(strong.get_text(strip=True))
if fund:
datum = fund.group(1)
break
verlag = _verlag_aus_titel(titel)
treffer.append(
QuellenTreffer(
titel=titel,
verlag=verlag,
autor=None,
erscheinungsdatum_oder_quartal=datum,
quellen_url=quellen_url,
bild_url=bild_aus_element(element, url),
)
)
folge_links: list[str] = []
for link in suppe.select(".pagination a[href]"):
ziel = _absolute_url(link["href"], url)
# Nur echte Paginierung dieser Liste folgen (?start=N),
# nicht den Druck-/Sprung-Links.
if ziel and "start=" in ziel:
folge_links.append(ziel)
return treffer, folge_links
def _verlag_aus_titel(titel: str) -> str | None:
"""„Verlag: Spielname“ → Verlag als Präfix vor dem Doppelpunkt."""
if ":" not in titel:
return None
praefix = titel.split(":", 1)[0].strip()
if 0 < len(praefix) <= 60:
return praefix
return None
def _absolute_url(href: str | None, basis_url: str) -> str | None:
if not href:
return None
from urllib.parse import urljoin
return urljoin(basis_url, href)