d02c9906a9
- GET /review-builder: Upload-Formular (Word .docx + Bilder Drag/Drop + Metadaten)
- POST /review-builder/build: Word parsen → LLM (DeepSeek Flash) strukturiert
Teaser/Spielerklärung/Fazit → HTML bauen → im Workspace speichern
- Bild-Resize: 1920x1080 @ 72dpi (PIL), Bilderverzeichnis/01/{buchstabe}/{spiel}/
- review_config.json: 25+ Autoren, 5 Disclosures, 5 Rubriken, Komplexitätsstufen
- Live-Publish via Joomla-API optional (Button: Vorschau / Veröffentlichen)
- Link im Dashboard: 📝 Review-Builder
229 lines
8.9 KiB
Python
229 lines
8.9 KiB
Python
#!/usr/bin/env python3
|
||
"""Generate the crowdfunding article HTML from merged data."""
|
||
import json, os, sys
|
||
from datetime import datetime, timezone
|
||
|
||
now = datetime.now(timezone.utc)
|
||
kw = now.isocalendar()[1]
|
||
today_de = now.strftime("%d. %B %Y")
|
||
# German month names
|
||
months_de = {"January":"Januar","February":"Februar","March":"März","April":"April","May":"Mai","June":"Juni",
|
||
"July":"Juli","August":"August","September":"September","October":"Oktober","November":"November","December":"Dezember"}
|
||
for en, de in months_de.items():
|
||
today_de = today_de.replace(en, de)
|
||
|
||
with open("/home/hermes/workspace/merged_data.json") as f:
|
||
data = json.load(f)
|
||
|
||
# Sort by USD funding
|
||
data.sort(key=lambda x: x["funding_usd"], reverse=True)
|
||
|
||
top10 = data[:10]
|
||
endspurte = [p for p in data if p["days_left"] <= 3 and p["funding_usd"] >= 500]
|
||
# Geheimtipps: well-funded smaller campaigns
|
||
geheimtipps = [p for p in data if p["funding_usd"] >= 1000 and p["pct"] >= 100
|
||
and p["backers"] >= 30 and p["funding_usd"] < 50000
|
||
and p not in top10]
|
||
|
||
def fmt_funds(p):
|
||
if p["currency"] == "EUR":
|
||
return f'{p["funds"]:,.0f} €'.replace(',', '.')
|
||
else:
|
||
return f'{p["funds"]:,.0f} $'.replace(',', '.')
|
||
|
||
def fmt_goal(p):
|
||
if p["currency"] == "EUR":
|
||
return f'{p["goal"]:,.0f} €'.replace(',', '.')
|
||
else:
|
||
return f'{p["goal"]:,.0f} $'.replace(',', '.')
|
||
|
||
def fmt_pct(p):
|
||
return f'{p["pct"]} %' if p["pct"] == int(p["pct"]) else f'{p["pct"]:.0f} %'
|
||
|
||
def days_class(d):
|
||
if d <= 1: return "days-urgent"
|
||
elif d <= 3: return "days-warning"
|
||
return "days-ok"
|
||
|
||
def rank_class(n):
|
||
if n == 1: return "rank-1"
|
||
elif n == 2: return "rank-2"
|
||
elif n == 3: return "rank-3"
|
||
return ""
|
||
|
||
def platform_cell(p):
|
||
if p["platform"] == "Gamefound":
|
||
return '<td style="text-align:center"><span class="badge"><img src="https://www.brettspiel-news.de/images/Spiele/2026/logo_gamefound.png" alt="Gamefound" /></span></td>'
|
||
else:
|
||
return '<td style="text-align:center"><span class="badge"><img src="https://www.brettspiel-news.de/images/Spiele/2026/logo_kickstarter.png" alt="Kickstarter" /></span></td>'
|
||
|
||
def cover_img(p):
|
||
if p["image"]:
|
||
# Use small thumbnail - Gamefound images might be too large, use inline CSS resize
|
||
return f'<img src="{p["image"]}" alt="{p["name"]}" style="width:60px;height:60px;object-fit:cover;border-radius:4px;" loading="lazy" />'
|
||
return ""
|
||
|
||
def game_cell(p, rank=None):
|
||
img_html = cover_img(p)
|
||
url = p["url"]
|
||
name = p['name']
|
||
if rank and rank <= 3:
|
||
name_html = f'<a href="{url}" target="_blank"><strong>{name}</strong></a>'
|
||
elif rank and rank <= 10:
|
||
name_html = f'<a href="{url}" target="_blank"><strong>{name}</strong></a>'
|
||
else:
|
||
name_html = f'<a href="{url}" target="_blank">{name}</a>'
|
||
|
||
if img_html:
|
||
return f'<td><div style="display:flex;align-items:center;gap:0.5rem;">{img_html}<div>{name_html}</div></div></td>'
|
||
else:
|
||
return f'<td>{name_html}</td>'
|
||
|
||
def table_row(p, rank=None, extra_class=""):
|
||
r = rank or 0
|
||
rc = rank_class(r) if r <= 3 else ""
|
||
badge = f'<span class="rank-badge {rc}">{r}</span>' if r else ""
|
||
|
||
cls = extra_class
|
||
dc = days_class(p["days_left"])
|
||
|
||
# Determine row class
|
||
if p["funding_usd"] >= 100000:
|
||
if "row-mega" not in cls:
|
||
cls = (cls + " row-mega").strip()
|
||
|
||
row = '<tr'
|
||
if cls:
|
||
row += f' class="{cls}"'
|
||
row += '>\n'
|
||
|
||
if rank:
|
||
row += f' <td class="rank">{badge}</td>\n'
|
||
|
||
row += f' {game_cell(p, rank=r)}\n'
|
||
row += f' {platform_cell(p)}\n'
|
||
row += f' <td class="funding">{fmt_funds(p)}</td>\n'
|
||
row += f' <td class="pct">{fmt_pct(p)}</td>\n'
|
||
row += f' <td class="num">{p["backers"]:,}</td>\n'.replace(',', '.')
|
||
row += f' <td class="num"><span class="{dc}">{p["days_left"]}</span></td>\n'
|
||
row += '</tr>'
|
||
return row
|
||
|
||
# === BUILD HTML ===
|
||
|
||
html = f"""<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta name="description" content="Crowdfunding-Übersicht KW {kw}: {len(data)} Brettspiel-Kampagnen auf Kickstarter und Gamefound im Check — Top 10, Endspurte und Geheimtipps.">
|
||
<meta name="keywords" content="Crowdfunding, Brettspiele, Kickstarter, Gamefound, KW {kw}">
|
||
<meta name="bsn-tags" content="Crowdfunding, Strategie, Kartenspiel">
|
||
<meta name="bsn-cover" content="images/Spiele/2026/crowdfunding_cover_1920x1080_2026_093650.jpg">
|
||
<title>Crowdfunding-Übersicht KW {kw} – {len(data)} Brettspiel-Kampagnen im Check</title>
|
||
</head>
|
||
<body>
|
||
|
||
<p class="meta">{today_de}</p>
|
||
<!-- bsn-meta:{today_de} -->
|
||
|
||
<p class="dachzeile">KW {kw}: {len(top10)} dicke Fische und {len(endspurte)} Endspurte — wir checken alle {len(data)} aktiven Brettspiel-Kampagnen auf Kickstarter und Gamefound.</p>
|
||
|
||
<article>
|
||
|
||
<p>Die KW {kw} bringt satte {len(data)} aktive Brettspiel-Kampagnen auf Kickstarter und Gamefound. Darunter echte Schwergewichte wie <strong>{top10[0]['name']}</strong> mit über {fmt_funds(top10[0])} — aber auch {sum(1 for p in data if p['days_left'] <= 1)} Kampagnen, die heute oder morgen auslaufen. Wir haben alle für euch im Blick.</p>
|
||
|
||
<hr id="system-readmore" />
|
||
"""
|
||
|
||
# === TOP 10 TABLE ===
|
||
html += '<h2>Die Großen: Top 10 nach Funding</h2>\n'
|
||
html += '<table class="bsn-table">\n'
|
||
html += '<tr><th>#</th><th>Spiel</th><th style="text-align:center">Plattform</th><th class="funding">Funding</th><th class="num">%</th><th class="num">Backer</th><th class="num">Tage</th></tr>\n'
|
||
|
||
for i, p in enumerate(top10):
|
||
extra = ""
|
||
# Mark mega campaigns
|
||
if p["funding_usd"] >= 250000:
|
||
extra = "row-mega"
|
||
elif p["funding_usd"] >= 100000:
|
||
extra = "row-highlight"
|
||
html += table_row(p, rank=i+1, extra_class=extra) + '\n'
|
||
|
||
html += '</table>\n\n'
|
||
|
||
# === ENDSPURTE ===
|
||
if endspurte:
|
||
es_sorted = sorted(endspurte, key=lambda x: x["days_left"])
|
||
html += '<h2>Endspurte: Diese Kampagnen laufen bald aus</h2>\n'
|
||
html += '<table class="bsn-table">\n'
|
||
html += '<tr><th>Spiel</th><th style="text-align:center">Plattform</th><th class="funding">Funding</th><th class="num">%</th><th class="num">Backer</th><th class="num">Tage</th></tr>\n'
|
||
|
||
for p in es_sorted:
|
||
extra = "row-highlight" if p["pct"] >= 100 else ""
|
||
html += table_row(p, extra_class=extra) + '\n'
|
||
|
||
html += '</table>\n\n'
|
||
|
||
# === GEHEIMTIPPS ===
|
||
if geheimtipps:
|
||
html += '<h2>Kleine Verlage, große Kampagnen</h2>\n'
|
||
html += '<p>Diese kleineren Projekte haben ihr Ziel bereits erreicht und verdienen einen zweiten Blick:</p>\n'
|
||
html += '<table class="bsn-table">\n'
|
||
html += '<tr><th>Spiel</th><th style="text-align:center">Plattform</th><th class="funding">Funding</th><th class="num">%</th><th class="num">Backer</th><th class="num">Tage</th></tr>\n'
|
||
|
||
for p in sorted(geheimtipps, key=lambda x: x["pct"], reverse=True)[:8]:
|
||
html += table_row(p, extra_class="row-highlight") + '\n'
|
||
|
||
html += '</table>\n\n'
|
||
|
||
# === ALL CAMPAIGNS ===
|
||
html += f'<h2>Alle {len(data)} Kampagnen im Überblick</h2>\n'
|
||
html += '<table class="bsn-table">\n'
|
||
html += '<tr><th>#</th><th>Spiel</th><th style="text-align:center">Plattform</th><th class="funding">Funding</th><th class="num">%</th><th class="num">Backer</th><th class="num">Tage</th></tr>\n'
|
||
|
||
for i, p in enumerate(data):
|
||
extra = ""
|
||
if i < 10:
|
||
extra = "row-highlight"
|
||
html += table_row(p, rank=i+1, extra_class=extra) + '\n'
|
||
|
||
html += '</table>\n\n'
|
||
|
||
# === SOURCES ===
|
||
html += f"""<span style="font-size:1.17em;font-weight:bold;display:block;margin:1em 0;"><strong>Quellen</strong></span>
|
||
<ol>
|
||
<li><a href="https://gamefound.com" target="_blank">Gamefound</a> — Aktive Crowdfunding-Projekte (Stand: {today_de})</li>
|
||
<li><a href="https://www.kickstarter.com" target="_blank">Kickstarter</a> — Tabletop Games Kategorie (Stand: {today_de})</li>
|
||
</ol>
|
||
|
||
<p class="bsn-recherche"><em>Recherchestand: {today_de}</em></p>
|
||
|
||
</article>
|
||
|
||
<!-- NANO-BANANA-PROMPT
|
||
Erstelle ein Titelbild für brettspiel-news.de — Crowdfunding-Übersicht KW {kw}:
|
||
- 16:9, 1920x1080, 72 DPI
|
||
- Stil: Editorial/Magazin, warme Beleuchtung
|
||
- Collage aus Brettspiel-Covern auf einem Holzspieltisch
|
||
- Im Zentrum: Logo-Integration "Crowdfunding KW {kw}"
|
||
- Kickstarter- und Gamefound-Logos dezent am Rand
|
||
- Farbpalette: warme Holztöne, BSN-Blau (#20228A) als Akzent
|
||
- Keine störenden Elemente im Hintergrund
|
||
-->
|
||
</body>
|
||
</html>"""
|
||
|
||
# Write output
|
||
out_path = "/home/hermes/workspace/crowdfunding_kw28_article.html"
|
||
with open(out_path, "w") as f:
|
||
f.write(html)
|
||
|
||
# Stats
|
||
print(f"Article written: {out_path}")
|
||
print(f"Projects: {len(data)} (GF: {sum(1 for p in data if p['platform']=='Gamefound')}, KS: {sum(1 for p in data if p['platform']=='Kickstarter')})")
|
||
print(f"Top 10 range: {fmt_funds(top10[-1])} — {fmt_funds(top10[0])}")
|
||
print(f"Endspurte: {len(endspurte)}")
|
||
print(f"Geheimtipps: {len(geheimtipps)}")
|
||
print("DONE")
|