fix: QC-Ratings aus 7 älteren Artikeln entfernt (CHECK 10)
This commit is contained in:
@@ -0,0 +1,509 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user