#!/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""" Fantasywelt Vorbestellungen – keine neuen Spiele

Keine neuen Vorbestellungen in den letzten {days} Tagen.

""" 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('DE') elif lang == "EN": badges.append('EN') elif lang == "Multi": badges.append('Multi') if bestprice == "Y": badges.append('Best-Preis') # Release-Datum prüfen (Zukunft = Warnung) release_badge = "" if p.get("release_dt") and p["release_dt"] > datetime.now(): release_badge = f' ⚠ ET: {release}' elif p.get("release_dt"): release_badge = f' ET: {release}' badge_html = "".join(badges) row = f""" {name} {badge_html}{release_badge} {price} € Vorbestellen → """ 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""" Fantasywelt Vorbestellungen – Neue Spiele der letzten {days} Tage

🎯 Neue Vorbestellungen bei Fantasywelt

Folgende Brett- und Kartenspiele sind in den letzten {days} Tagen neu ins Fantasywelt-Sortiment aufgenommen worden und aktuell als Vorbestellung gelistet (nicht sofort lieferbar). Stand: {datetime.now().strftime('%d.%m.%Y')}

Gesamt: {len(vorbestellungen)}
🇩🇪 DE: {de_count}
🇬🇧 EN: {en_count}
🌐 Multi: {multi_count}
{table_html}
SpielPreis

Alle Preise inkl. MwSt. · Links sind Affiliate-Links (bsn=ig1UxQ0L2cNMNslzFhyr) · Datenquelle: Fantasywelt CSV-Export

""" # ─── 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 += 'DE' elif lang == 'EN': badges += 'EN' elif lang in ('Multi','DE/EN'): badges += 'Multi' if p["bestprice"] == "Y": badges += 'Best-Preis' et_badge = '' if p.get("release_dt") and p["release_dt"] > datetime.now(): et_badge = f' ⚠ ET: {release}' elif p.get("release_dt"): et_badge = f' ET: {release}' row = f''' {name} {badges}{et_badge} {price} € Vorbestellen → ''' 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''' Fantasywelt Vorbestellungen: {len(vorbestellungen)} neue Spiele vorbestellbar (KW {kw})

Fantasywelt Vorbestellungen: {len(vorbestellungen)} neue Spiele vorbestellbar (KW {kw})

{heute} · Vorbestellungen · Automatisiert

In den letzten 30 Tagen hat Fantasywelt {len(vorbestellungen)} neue Brett- und Kartenspiele ins Sortiment aufgenommen, die aktuell ausschließlich als Vorbestellung gelistet sind — also nicht sofort lieferbar, aber reservierbar.


Gesamt: {len(vorbestellungen)}
🇩🇪 DE: {de_count}
🇬🇧 EN: {en_count}
🌐 Multi: {multi_count}
{table}
SpielPreis

QC: ✅ {len(vorbestellungen)} Vorbestellungen via CSV-Export · ✅ Zubehör/Non-Games ausgefiltert · ✅ Affiliate-Links (bsn=ig1UxQ0L2cNMNslzFhyr) · ✅ Automatisiert jeden Freitag

''' 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()