510 lines
23 KiB
Python
510 lines
23 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Fantasywelt.de Vorbestellungs-Tracker
|
||
=====================================
|
||
Ermittelt alle Spiele, die in den letzten 30 Tagen neu ins Sortiment
|
||
gekommen und als Vorbestellung (nicht lieferbar) gelistet sind.
|
||
|
||
Nutzung:
|
||
python3 fantasywelt_vorbestellungen.py # Einmal-Ausführung
|
||
python3 fantasywelt_vorbestellungen.py --days 30 # Zeitraum anpassen
|
||
|
||
Output:
|
||
- HTML: /home/hermes/workspace/fantasywelt_data/vorbestellungen.html
|
||
- Terminal: Zusammenfassung
|
||
"""
|
||
|
||
import csv, io, json, os, sys, urllib.request, urllib.error
|
||
from datetime import datetime, timedelta
|
||
from pathlib import Path
|
||
from html import escape
|
||
|
||
# ─── KONFIGURATION ───────────────────────────────────────────────────────────
|
||
|
||
CSV_URL = "https://www.fantasywelt.de/export/brettspiele.csv"
|
||
DATA_DIR = Path("/home/hermes/workspace/fantasywelt_data")
|
||
OUTPUT_FILE = DATA_DIR / "vorbestellungen.html"
|
||
AFFILIATE_TAG = "bsn=ig1UxQ0L2cNMNslzFhyr"
|
||
DEFAULT_DAYS = 30
|
||
|
||
# Aus dem Haupt-Tracker übernommene Filter
|
||
ZUBEHOER_KEYWORDS = [
|
||
"schutzhüll", "sleeve", "hülle", "hüllen", "deckbox", "deck box", "deck-box",
|
||
"schaumstoff", "feldherr", "portfolio", "zipfolio", "flexxfolio", "play-mat",
|
||
"playmat", "play mat", "toploader", "card holder", "card divider",
|
||
"pages ", "page ", "pocket pages", "kartenbox", "card case", "card box",
|
||
"faltkarton", "lagerbox", "gaming case", "gaming box",
|
||
"pro-matte", "pro-fit", "ultra pro", "ultra-snug",
|
||
"dragon shield", "board game sleeves", "board games sleeves",
|
||
"kmc small", "kmc standard", "precise-fit", "soft sleeves",
|
||
"9-pocket", "4-pocket", "8-pocket", "12-pocket", "16-pocket", "18-pocket",
|
||
"e-raptor spieler-tableau", "organizer", "würfel", "dice tower",
|
||
"raster schaumstoff", "schaumstoffeinlage", "schaumstoffzuschnitt",
|
||
"schaumstoff-set", "schaumstofeinlage", "schaumstoff abdeckung",
|
||
"half-size schaumstoff", "full-size schaumstoff", "tasche für", "medium tasche",
|
||
"miniatures pack", "playerboard", "game mat", "neoprene",
|
||
"sammelalbum", "ultimate guard", "sidewinder", "omnihive", "xenoskin",
|
||
"spielmatte", "playmat", "puzzle", "ausgrabung", "kristall",
|
||
"farblabor", "experimentier", "deck protector", "boulder",
|
||
"insert ", "insert für", "card backpack", "card case",
|
||
"collector's edition small card", "collector's edition dnd",
|
||
"enhance ", "backpack", "miniaturen-set", "miniaturen set",
|
||
"miniatures set", "token set", "token kit", "acryl",
|
||
"metallmünzen", "metal coins", "coin set", "münzset",
|
||
"spielmünzen", "upgrade pack", "upgrade kit", "conversion kit",
|
||
"bag of", "dice set", "würfelset", "würfel-set", "dice pack",
|
||
"laserox", "insert", "inlay", "organizer", "einsatz",
|
||
]
|
||
|
||
EXCLUDE_KEYWORDS = ["mängelexemplar", "mängelexemplare", "mangelexemplar"]
|
||
|
||
# ─── HILFSFUNKTIONEN ─────────────────────────────────────────────────────────
|
||
|
||
def should_skip(name):
|
||
n = name.lower()
|
||
return any(kw in n for kw in ZUBEHOER_KEYWORDS) or any(kw in n for kw in EXCLUDE_KEYWORDS)
|
||
|
||
def add_affiliate(url):
|
||
if not url:
|
||
return url
|
||
return url + ("&" if "?" in url else "?") + AFFILIATE_TAG
|
||
|
||
def download_csv():
|
||
req = urllib.request.Request(CSV_URL, headers={"User-Agent": "BSN-Vorbestellungs-Tracker/1.0"})
|
||
with urllib.request.urlopen(req, timeout=60) as resp:
|
||
data = resp.read()
|
||
try:
|
||
return data.decode("utf-8")
|
||
except UnicodeDecodeError:
|
||
return data.decode("latin-1")
|
||
|
||
def parse_csv(raw):
|
||
reader = csv.DictReader(io.StringIO(raw), delimiter=";", quotechar='"')
|
||
products = []
|
||
for row in reader:
|
||
name = row.get("Name", "").strip()
|
||
if not name:
|
||
continue
|
||
products.append({
|
||
"name": name,
|
||
"sku": row.get("Artikelnummer", "").strip(),
|
||
"ean": row.get("EAN", "").strip(),
|
||
"price": row.get("VK/Sonderpreis", "0").strip(),
|
||
"available": row.get("Verfügbarkeit", "").strip(),
|
||
"supply": row.get("Lieferstatus", "").strip(),
|
||
"created": row.get("Erstellt am", "").strip(),
|
||
"release": row.get("Erscheint am", "").strip(),
|
||
"url": row.get("Produkt-URL", "").strip(),
|
||
"image": row.get("Bild-URL", "").strip(),
|
||
"bestprice": row.get("Best-Price_Garantie!", "N").strip(),
|
||
})
|
||
return products
|
||
|
||
def format_price(s):
|
||
try:
|
||
return f"{float(s.replace(',', '.')):.2f}".replace(".", ",")
|
||
except:
|
||
return s
|
||
|
||
def detect_language(name):
|
||
n = name.upper()
|
||
if "(DE|EN)" in n or "(DEEN)" in n or "(DE/EN)" in n:
|
||
return "DE/EN"
|
||
elif "(DE)" in n or "(DEUTSCH)" in n:
|
||
return "DE"
|
||
elif "(EN)" in n or "(ENGLISH)" in n or "(ENGLISCH)" in n:
|
||
return "EN"
|
||
elif "MULTILINGUAL" in n:
|
||
return "Multi"
|
||
return ""
|
||
|
||
def parse_date(s):
|
||
"""Parse YYYY-MM-DD oder YYYY-MM-DD HH:MM:SS"""
|
||
s = s.strip()
|
||
for fmt in ["%Y-%m-%d", "%Y-%m-%d %H:%M:%S"]:
|
||
try:
|
||
return datetime.strptime(s, fmt)
|
||
except:
|
||
continue
|
||
return None
|
||
|
||
# ─── KERN-LOGIK ──────────────────────────────────────────────────────────────
|
||
|
||
def find_new_preorders(days=30):
|
||
"""Findet alle neuen Vorbestellungen der letzten N Tage."""
|
||
cutoff = datetime.now() - timedelta(days=days)
|
||
|
||
raw = download_csv()
|
||
products = parse_csv(raw)
|
||
|
||
vorbestellungen = []
|
||
skipped_zubehoer = 0
|
||
|
||
for p in products:
|
||
# Nur Vorbestellungen
|
||
if p["supply"] != "Vorbestellung":
|
||
continue
|
||
|
||
# Zubehör/Non-Games filtern
|
||
if should_skip(p["name"]):
|
||
skipped_zubehoer += 1
|
||
continue
|
||
|
||
# Erstellungsdatum prüfen
|
||
created = parse_date(p["created"])
|
||
if created is None or created < cutoff:
|
||
continue
|
||
|
||
# Release-Datum parsen
|
||
release_dt = parse_date(p["release"]) if p["release"] else None
|
||
release_str = release_dt.strftime("%d.%m.%Y") if release_dt else (p["release"] or "?")
|
||
|
||
vorbestellungen.append({
|
||
"name": p["name"],
|
||
"price": p["price"],
|
||
"url": p["url"],
|
||
"image": p["image"],
|
||
"created": created.strftime("%Y-%m-%d"),
|
||
"release": release_str,
|
||
"release_dt": release_dt,
|
||
"bestprice": p["bestprice"],
|
||
})
|
||
|
||
# Sortieren: DE zuerst, dann nach Erstellungsdatum
|
||
vorbestellungen.sort(key=lambda x: (
|
||
detect_language(x["name"]) != "DE",
|
||
detect_language(x["name"]) == "EN",
|
||
x["created"]
|
||
))
|
||
|
||
return vorbestellungen, skipped_zubehoer
|
||
|
||
# ─── HTML-GENERIERUNG ────────────────────────────────────────────────────────
|
||
|
||
def render_html(vorbestellungen, days):
|
||
"""Erzeugt eine saubere HTML-Übersicht im BSN-Stil."""
|
||
|
||
if not vorbestellungen:
|
||
return f"""<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Fantasywelt Vorbestellungen – keine neuen Spiele</title>
|
||
<style>
|
||
body {{ margin:0; padding:0; background:#f9f7f4; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; color:#2c2416; line-height:1.75; }}
|
||
article {{ max-width:720px; margin:0 auto; padding:3rem 1.5rem; background:#fff; box-shadow:0 2px 12px rgba(0,0,0,0.06); }}
|
||
</style>
|
||
</head>
|
||
<body><article>
|
||
<p style="color:#6b7280;text-align:center;padding:2rem 0;">Keine neuen Vorbestellungen in den letzten {days} Tagen.</p>
|
||
</article></body></html>"""
|
||
|
||
rows = []
|
||
for i, p in enumerate(vorbestellungen):
|
||
name = escape(p["name"])
|
||
price = format_price(p["price"])
|
||
url = escape(add_affiliate(p["url"]))
|
||
lang = detect_language(p["name"])
|
||
release = p["release"]
|
||
bestprice = p["bestprice"]
|
||
|
||
# Badges
|
||
badges = []
|
||
if lang == "DE":
|
||
badges.append('<span style="display:inline-block;background:#20228a;color:#fff;padding:1px 6px;border-radius:3px;font-size:0.7rem;margin-left:4px;">DE</span>')
|
||
elif lang == "EN":
|
||
badges.append('<span style="display:inline-block;background:#6b7280;color:#fff;padding:1px 6px;border-radius:3px;font-size:0.7rem;margin-left:4px;">EN</span>')
|
||
elif lang == "Multi":
|
||
badges.append('<span style="display:inline-block;background:#10b981;color:#fff;padding:1px 6px;border-radius:3px;font-size:0.7rem;margin-left:4px;">Multi</span>')
|
||
|
||
if bestprice == "Y":
|
||
badges.append('<span style="display:inline-block;background:#f59e0b;color:#fff;padding:1px 6px;border-radius:3px;font-size:0.7rem;margin-left:4px;">Best-Preis</span>')
|
||
|
||
# Release-Datum prüfen (Zukunft = Warnung)
|
||
release_badge = ""
|
||
if p.get("release_dt") and p["release_dt"] > datetime.now():
|
||
release_badge = f' <span style="display:inline-block;background:#dc3545;color:#fff;padding:1px 6px;border-radius:3px;font-size:0.7rem;margin-left:4px;">⚠ ET: {release}</span>'
|
||
elif p.get("release_dt"):
|
||
release_badge = f' <span style="display:inline-block;background:#e8e0d3;color:#5a4a3a;padding:1px 6px;border-radius:3px;font-size:0.7rem;margin-left:4px;">ET: {release}</span>'
|
||
|
||
badge_html = "".join(badges)
|
||
|
||
row = f"""<tr class="{'even' if i % 2 == 1 else ''}">
|
||
<td style="padding:0.6rem 0.7rem;border-bottom:1px solid #e8e0d3;">
|
||
<a href="{url}" target="_blank" rel="sponsored" style="color:#c75b3a;font-weight:600;text-decoration:none;">{name}</a>
|
||
{badge_html}{release_badge}
|
||
</td>
|
||
<td style="text-align:right;padding:0.6rem 0.7rem;border-bottom:1px solid #e8e0d3;white-space:nowrap;font-weight:700;color:#20228a;">{price} €</td>
|
||
<td style="text-align:center;padding:0.6rem 0.7rem;border-bottom:1px solid #e8e0d3;">
|
||
<a href="{url}" target="_blank" rel="sponsored" style="display:inline-block;background:#20228a;color:#fff;text-decoration:none;padding:5px 14px;border-radius:99px;font-size:0.8rem;font-weight:700;white-space:nowrap;">Vorbestellen →</a>
|
||
</td>
|
||
</tr>"""
|
||
rows.append(row)
|
||
|
||
table_html = "\n".join(rows)
|
||
|
||
de_count = sum(1 for p in vorbestellungen if detect_language(p["name"]) == "DE")
|
||
en_count = sum(1 for p in vorbestellungen if detect_language(p["name"]) == "EN")
|
||
multi_count = sum(1 for p in vorbestellungen if detect_language(p["name"]) in ("Multi", "DE/EN"))
|
||
|
||
return f"""<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Fantasywelt Vorbestellungen – Neue Spiele der letzten {days} Tage</title>
|
||
<style>
|
||
body {{ margin:0; padding:0; background:#f9f7f4; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; color:#2c2416; line-height:1.75; }}
|
||
article {{ max-width:720px; margin:0 auto; padding:2rem 1.5rem; background:#fff; box-shadow:0 2px 12px rgba(0,0,0,0.06); }}
|
||
h1 {{ font-size:1.5rem; margin-top:0; color:#1a1a1a; }}
|
||
h2 {{ font-size:1.2rem; margin:1.5rem 0 0.75rem; color:#333; border-bottom:2px solid #e8e0d3; padding-bottom:0.3rem; }}
|
||
.stats {{ display:flex; gap:1rem; flex-wrap:wrap; margin:1rem 0 1.5rem; }}
|
||
.stat {{ background:#f0ebe0; border-radius:8px; padding:0.4rem 1rem; font-size:0.85rem; }}
|
||
.stat strong {{ color:#20228a; }}
|
||
table {{ width:100%; border-collapse:collapse; margin:1rem 0; font-size:0.9rem; }}
|
||
th {{ background:#f0ebe0; text-align:left; padding:0.5rem 0.7rem; border-bottom:2px solid #d5c9b3; font-weight:600; }}
|
||
th:last-child {{ text-align:center; }}
|
||
tr.even td {{ background:#faf7f2; }}
|
||
.qc {{ font-size:0.75rem; color:#aaa; margin-top:2rem; border-top:1px solid #e8e0d3; padding-top:0.8rem; }}
|
||
@media (max-width:500px) {{
|
||
article {{ padding:1rem 0.7rem; }}
|
||
td, th {{ font-size:0.8rem; padding:0.4rem; }}
|
||
}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<article>
|
||
|
||
<h1>🎯 Neue Vorbestellungen bei Fantasywelt</h1>
|
||
<p style="color:#6b7280;font-size:0.85rem;margin-bottom:1.5rem;">
|
||
Folgende Brett- und Kartenspiele sind in den letzten {days} Tagen neu ins Fantasywelt-Sortiment aufgenommen worden
|
||
und aktuell als <strong>Vorbestellung</strong> gelistet (nicht sofort lieferbar).
|
||
Stand: {datetime.now().strftime('%d.%m.%Y')}
|
||
</p>
|
||
|
||
<div class="stats">
|
||
<div class="stat">Gesamt: <strong>{len(vorbestellungen)}</strong></div>
|
||
<div class="stat">🇩🇪 DE: <strong>{de_count}</strong></div>
|
||
<div class="stat">🇬🇧 EN: <strong>{en_count}</strong></div>
|
||
<div class="stat">🌐 Multi: <strong>{multi_count}</strong></div>
|
||
</div>
|
||
|
||
<table>
|
||
<thead>
|
||
<tr><th>Spiel</th><th style="text-align:right;">Preis</th><th style="text-align:center;"></th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{table_html}
|
||
</tbody>
|
||
</table>
|
||
|
||
<p class="qc">Alle Preise inkl. MwSt. · Links sind Affiliate-Links (bsn=ig1UxQ0L2cNMNslzFhyr) · Datenquelle: <a href="https://www.fantasywelt.de/export/brettspiele.csv">Fantasywelt CSV-Export</a></p>
|
||
|
||
</article>
|
||
</body>
|
||
</html>"""
|
||
|
||
# ─── MAIN ────────────────────────────────────────────────────────────────────
|
||
|
||
def build_article_html(vorbestellungen):
|
||
"""Baut den kompletten Joomla-Artikel mit ALLEN Vorbestellungen."""
|
||
from html import escape as esc
|
||
|
||
rows = []
|
||
for p in vorbestellungen:
|
||
name = esc(p["name"])
|
||
price = format_price(p["price"])
|
||
url = esc(add_affiliate(p["url"]))
|
||
lang = detect_language(p["name"])
|
||
release = p["release"]
|
||
|
||
badges = ''
|
||
if lang == 'DE':
|
||
badges += '<span style="display:inline-block;background:#20228a;color:#fff;padding:1px 6px;border-radius:3px;font-size:0.7rem;margin-left:4px;">DE</span>'
|
||
elif lang == 'EN':
|
||
badges += '<span style="display:inline-block;background:#6b7280;color:#fff;padding:1px 6px;border-radius:3px;font-size:0.7rem;margin-left:4px;">EN</span>'
|
||
elif lang in ('Multi','DE/EN'):
|
||
badges += '<span style="display:inline-block;background:#10b981;color:#fff;padding:1px 6px;border-radius:3px;font-size:0.7rem;margin-left:4px;">Multi</span>'
|
||
|
||
if p["bestprice"] == "Y":
|
||
badges += '<span style="display:inline-block;background:#f59e0b;color:#fff;padding:1px 6px;border-radius:3px;font-size:0.7rem;margin-left:4px;">Best-Preis</span>'
|
||
|
||
et_badge = ''
|
||
if p.get("release_dt") and p["release_dt"] > datetime.now():
|
||
et_badge = f' <span style="display:inline-block;background:#dc3545;color:#fff;padding:1px 6px;border-radius:3px;font-size:0.7rem;margin-left:4px;">⚠ ET: {release}</span>'
|
||
elif p.get("release_dt"):
|
||
et_badge = f' <span style="display:inline-block;background:#e8e0d3;color:#5a4a3a;padding:1px 6px;border-radius:3px;font-size:0.7rem;margin-left:4px;">ET: {release}</span>'
|
||
|
||
row = f'''<tr>
|
||
<td style="padding:0.5rem 0.7rem;border-bottom:1px solid #e8e0d3;">
|
||
<a href="{url}" target="_blank" rel="sponsored" style="color:#c75b3a;font-weight:600;">{name}</a>
|
||
{badges}{et_badge}
|
||
</td>
|
||
<td style="text-align:right;padding:0.5rem 0.7rem;border-bottom:1px solid #e8e0d3;white-space:nowrap;font-weight:700;color:#20228a;">{price} €</td>
|
||
<td style="text-align:center;padding:0.5rem 0.7rem;border-bottom:1px solid #e8e0d3;">
|
||
<a href="{url}" target="_blank" rel="sponsored" style="display:inline-block;background:#20228a;color:#fff;text-decoration:none;padding:4px 12px;border-radius:99px;font-size:0.8rem;font-weight:700;">Vorbestellen →</a>
|
||
</td>
|
||
</tr>'''
|
||
rows.append(row)
|
||
|
||
table = '\n'.join(rows)
|
||
de_count = sum(1 for p in vorbestellungen if detect_language(p["name"]) == "DE")
|
||
en_count = sum(1 for p in vorbestellungen if detect_language(p["name"]) == "EN")
|
||
multi_count = len(vorbestellungen) - de_count - en_count
|
||
heute = datetime.now().strftime("%d.%m.%Y")
|
||
kw = datetime.now().isocalendar()[1]
|
||
|
||
return 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="{len(vorbestellungen)} neue Brett- und Kartenspiele sind in den letzten 30 Tagen bei Fantasywelt als Vorbestellung gelistet. Alle Preise, Erscheinungstermine und Direktlinks.">
|
||
<meta name="keywords" content="Fantasywelt, Vorbestellungen, Brettspiele, Neuerscheinungen, Preorder">
|
||
<meta name="joomla-id" content="15378">
|
||
<title>Fantasywelt Vorbestellungen: {len(vorbestellungen)} neue Spiele vorbestellbar (KW {kw})</title>
|
||
<style>
|
||
body {{ margin: 0; padding: 0; background: #f9f7f4; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #2c2416; line-height: 1.75; }}
|
||
article {{ max-width: 720px; margin: 0 auto; padding: 2rem 1.5rem; background: #fff; box-shadow: 0 2px 12px rgba(0,0,0,0.06); }}
|
||
h1 {{ font-size: 1.6rem; margin-top: 0; color: #1a1a1a; }}
|
||
p {{ margin: 0 0 1rem; }}
|
||
a {{ color: #c75b3a; text-decoration: none; }}
|
||
a:hover {{ text-decoration: underline; }}
|
||
.meta {{ color: #999; font-size: 0.85rem; margin-bottom: 2rem; }}
|
||
.stats {{ display: flex; gap: 1rem; flex-wrap: wrap; margin: 1rem 0 1.5rem; }}
|
||
.stat {{ background: #f0ebe0; border-radius: 8px; padding: 0.4rem 1rem; font-size: 0.85rem; }}
|
||
.stat strong {{ color: #20228a; }}
|
||
table {{ width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: 0.9rem; }}
|
||
th {{ background: #f0ebe0; text-align: left; padding: 0.5rem 0.7rem; border-bottom: 2px solid #d5c9b3; font-weight: 600; }}
|
||
td {{ padding: 0.5rem 0.7rem; border-bottom: 1px solid #e8e0d3; }}
|
||
tr:nth-child(even) td {{ background: #faf7f2; }}
|
||
.qc {{ font-size: 0.75rem; color: #aaa; margin-top: 2rem; border-top: 1px solid #e8e0d3; padding-top: 0.8rem; }}
|
||
@media (max-width: 500px) {{
|
||
article {{ padding: 0.8rem 0.5rem; }}
|
||
td, th {{ font-size: 0.78rem; padding: 0.35rem; }}
|
||
}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<article>
|
||
|
||
<h1>Fantasywelt Vorbestellungen: {len(vorbestellungen)} neue Spiele vorbestellbar (KW {kw})</h1>
|
||
<p class="meta">{heute} · Vorbestellungen · Automatisiert</p>
|
||
|
||
<p>In den letzten 30 Tagen hat Fantasywelt {len(vorbestellungen)} neue Brett- und Kartenspiele ins Sortiment aufgenommen, die aktuell ausschließlich als <strong>Vorbestellung</strong> gelistet sind — also nicht sofort lieferbar, aber reservierbar.</p>
|
||
|
||
<hr id="system-readmore" />
|
||
|
||
<div class="stats">
|
||
<div class="stat">Gesamt: <strong>{len(vorbestellungen)}</strong></div>
|
||
<div class="stat">🇩🇪 DE: <strong>{de_count}</strong></div>
|
||
<div class="stat">🇬🇧 EN: <strong>{en_count}</strong></div>
|
||
<div class="stat">🌐 Multi: <strong>{multi_count}</strong></div>
|
||
</div>
|
||
|
||
<table>
|
||
<thead>
|
||
<tr><th>Spiel</th><th style="text-align:right;">Preis</th><th style="text-align:center;"></th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{table}
|
||
</tbody>
|
||
</table>
|
||
|
||
<p class="qc">QC: ✅ {len(vorbestellungen)} Vorbestellungen via CSV-Export · ✅ Zubehör/Non-Games ausgefiltert · ✅ Affiliate-Links (bsn=ig1UxQ0L2cNMNslzFhyr) · ✅ Automatisiert jeden Freitag</p>
|
||
|
||
</article>
|
||
</body>
|
||
</html>'''
|
||
|
||
|
||
def publish_article(html_content, article_filename="artikel_fantasywelt_vorbestellungen_auto.html"):
|
||
"""Sendet den Artikel-HTML an den Article-Server (localhost:8890/publish)."""
|
||
import urllib.request, base64, json as _json
|
||
|
||
article_path = DATA_DIR.parent / article_filename
|
||
with open(article_path, "w", encoding="utf-8") as f:
|
||
f.write(html_content)
|
||
|
||
auth = base64.b64encode(b"DK-Adminchef:Mcik7%vbdhXa_").decode()
|
||
payload = _json.dumps({"files": [article_filename], "live": False}).encode()
|
||
|
||
req = urllib.request.Request(
|
||
f"http://localhost:8890/publish?key=br3ttsp1el-n3ws-2026",
|
||
data=payload,
|
||
headers={
|
||
"Authorization": f"Basic {auth}",
|
||
"Content-Type": "application/json",
|
||
},
|
||
method="POST"
|
||
)
|
||
|
||
try:
|
||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||
result = _json.loads(resp.read())
|
||
return result
|
||
except Exception as e:
|
||
return {"ok": False, "error": str(e)}
|
||
|
||
|
||
def main():
|
||
import argparse
|
||
parser = argparse.ArgumentParser(description="Fantasywelt Vorbestellungs-Tracker")
|
||
parser.add_argument("--days", type=int, default=DEFAULT_DAYS)
|
||
parser.add_argument("--publish-article", action="store_true")
|
||
args = parser.parse_args()
|
||
days = args.days
|
||
|
||
print(f"🔍 Suche neue Vorbestellungen der letzten {days} Tage...")
|
||
|
||
vorbestellungen, skipped = find_new_preorders(days)
|
||
|
||
print(f"✅ {len(vorbestellungen)} neue vorbestellbare Spiele gefunden")
|
||
print(f"🚫 {skipped} Zubehör-Artikel ausgefiltert")
|
||
print()
|
||
|
||
# Kurzübersicht für Terminal
|
||
de_games = [p for p in vorbestellungen if detect_language(p["name"]) == "DE"]
|
||
en_games = [p for p in vorbestellungen if detect_language(p["name"]) == "EN"]
|
||
|
||
if de_games:
|
||
print(f"─── DE-Vorbestellungen ({len(de_games)}) ───")
|
||
for p in de_games[:15]:
|
||
print(f" {p['name'][:60]} | {format_price(p['price'])} € | ET: {p['release']}")
|
||
if len(de_games) > 15:
|
||
print(f" ... und {len(de_games) - 15} weitere")
|
||
|
||
if en_games:
|
||
print(f"\n─── EN-Vorbestellungen ({len(en_games)}) ───")
|
||
for p in en_games[:5]:
|
||
print(f" {p['name'][:60]} | {format_price(p['price'])} € | ET: {p['release']}")
|
||
if len(en_games) > 5:
|
||
print(f" ... und {len(en_games) - 5} weitere")
|
||
|
||
# HTML-Übersicht (immer)
|
||
html = render_html(vorbestellungen, days)
|
||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
|
||
f.write(html)
|
||
print(f"\n📄 HTML-Übersicht: {OUTPUT_FILE}")
|
||
|
||
# Joomla-Artikel publizieren
|
||
if args.publish_article:
|
||
print("\n📰 Baue kompletten Artikel mit allen Spielen...")
|
||
article_html = build_article_html(vorbestellungen)
|
||
result = publish_article(article_html)
|
||
if result.get("ok"):
|
||
print(f"✅ Artikel 15378 aktualisiert ({len(vorbestellungen)} Spiele)")
|
||
if result.get("errors"):
|
||
for e in result["errors"]:
|
||
print(f" ⚠️ {e}")
|
||
else:
|
||
print(f"❌ Fehler: {result.get('error', 'unbekannt')}")
|
||
else:
|
||
print(f"🌐 http://185.162.249.159:8890/fantasywelt_data/vorbestellungen.html?key=br3ttsp1el-n3ws-2026")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|