Keine neuen Vorbestellungen in den letzten {days} Tagen.
#!/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"""
Keine neuen Vorbestellungen in den letzten {days} Tagen.
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')}
| Spiel | Preis |
|---|
Alle Preise inkl. MwSt. · Links sind Affiliate-Links (bsn=ig1UxQ0L2cNMNslzFhyr) · Datenquelle: Fantasywelt CSV-Export
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.
| Spiel | Preis |
|---|
QC: ✅ {len(vorbestellungen)} Vorbestellungen via CSV-Export · ✅ Zubehör/Non-Games ausgefiltert · ✅ Affiliate-Links (bsn=ig1UxQ0L2cNMNslzFhyr) · ✅ Automatisiert jeden Freitag