Files
BSN-Chatsystem/search_kinderspiele.py
T

310 lines
12 KiB
Python

#!/usr/bin/env python3
"""Amazon CreatorAPI 3.2 — Kinderspiele-Deals auf amazon.de finden.
NUR Kinderspiele behalten, Non-Games entfernen."""
import argparse, os, re, time
from datetime import datetime
from amazon_creatorsapi import AmazonCreatorsApi, Country
from amazon_creatorsapi import models as m
CREDENTIAL_ID = "amzn1.application-oa2-client.91799968d0744e66950ea413659197d4"
CREDENTIAL_SECRET = "amzn1.oa2-cs.v1.4c41154ecfd1835e00879ad5c8060ef5cd3b209b06077c0af1d868b343cab955"
PARTNER_TAG = "60pro05-21"
BROWSE_NODE = "1290250031" # Gesellschaftsspiele
COUNTRY = Country.DE
MAX_PAGES = 8
ITEMS_PER_PAGE = 50
KINDERSPIEL_KEYWORDS = [
"Kinderspiel Brettspiel",
"Kinderspiel ab 3 Jahre",
"Kinderspiel ab 4 Jahre",
"Kinderspiel ab 5 Jahre",
"Kinderspiel ab 6 Jahre",
"Kinderspiel ab 7 Jahre",
"Mitbringspiel",
"Vorschulspiel",
"Lernspiel Kinder",
"Gedächtnisspiel Kinder",
"Würfelspiel Kinder",
"Kinderkartenspiel",
"Bluey Spiel",
"Paw Patrol Spiel",
"Peppa Pig Spiel",
"Sendung mit der Maus Spiel",
"Dobble",
"Uno Junior",
"Lotti Karotti",
"Obstgarten Spiel",
]
RESOURCES = [
m.SearchItemsResource.ITEM_INFO_DOT_TITLE,
m.SearchItemsResource.IMAGES_DOT_PRIMARY_DOT_LARGE,
m.SearchItemsResource.OFFERS_V2_DOT_LISTINGS_DOT_PRICE,
]
# Patterns für NON-Games (entfernen)
NON_GAME_PATTERNS = [
r'\btragetasche\b', r'\bau(fbewahr|fbewahrung)\b', r'\bpuzzle\b',
r'\bjigsaw\b', r'\bairtag\b', r'\bwiso\s+steuer\b', r'\bblu-?ray\b',
r'\bdvd\b', r'\binsert\b', r'\borgani[sz]er\b', r'\bwellness\b',
r'\bclean\s+slate\b', r'\btaschenbuch\b', r'\bcd\b', r'\bbuch\b',
r'\bhandgepäck\b', r'\beurowings\b', r'\bryanair\b', r'\bsoaker\b',
r'\bnerf\b', r'\bkrang\b', r'\bdr\.?\s*rahm\b',
r'\d{2,4}\s*teilig', # Puzzles mit Stückzahl
r'\d{3,4}\s*pc\b', # Jigsaw piece count
]
NON_GAME_SIMPLE = [
'tragetasche', 'aufbewahrung', 'airtag', 'wiso', 'blu-ray', 'bluray',
'wellness', 'clean slate', 'dr. rahm', 'krang', 'handgepäck',
'eurowings', 'ryanair', 'soaker', 'nerf', 'taschenbuch',
'insert', 'organizer', 'upgrade kit',
]
def search_amazon(api, keyword, page=1):
try:
kwargs = dict(
browse_node_id=BROWSE_NODE,
item_count=ITEMS_PER_PAGE,
item_page=page,
resources=RESOURCES,
)
if keyword:
kwargs["keywords"] = keyword
resp = api.search_items(**kwargs)
time.sleep(1.1)
return resp
except Exception as e:
print(f" Error '{keyword}' p{page}: {e}", flush=True)
return None
def is_nongame(title):
"""Check if title is a non-game product."""
tl = title.lower()
# Simple check
for bad in NON_GAME_SIMPLE:
if bad in tl:
return True
# Stückzahl-Puzzle check: "1000 Teile", "500-teilig" etc.
if re.search(r'\b(500|1000|1500|2000)\s*[tT]eil', tl):
return True
return False
def is_kinderspiel(title):
"""Check if title is a genuine children's game."""
tl = title.lower()
# Age markers
if re.search(r'ab\s+[3-8]\s+jahr', tl):
return True
# Children's game markers
kind_markers = [
'kinderspiel', 'mitbringspiel', 'vorschul', 'lernspiel',
'gedächtnisspiel', 'kinderkartenspiel', 'würfelspiel',
'junior', 'kids', 'kinder',
'bluey', 'paw patrol', 'peppa pig', 'sendung mit der maus',
'monsterjäger', 'pupsparade', 'sagaland', 'verrückte labyrinth',
'paletti spaghetti', 'tempo kleine schnecke', 'dobble',
'uno junior', 'lotti karotti', 'obstgarten',
'halli galli', 'zicke zacke', 'leo muss zum friseur',
'make n break', 'make \'n\' break', 'memo', 'memory',
]
for marker in kind_markers:
if marker in tl:
return True
return False
def extract_deals(response):
deals = []
if not response or not response.items:
return deals
for item in response.items:
try:
title = str(item.item_info.title.display_value)
# QC: Non-Games entfernen
if is_nongame(title):
continue
# QC: Nur Kinderspiele behalten
if not is_kinderspiel(title):
continue
image = ""
if item.images and item.images.primary and item.images.primary.large:
image = str(item.images.primary.large.url)
asin = str(item.asin)
if not item.offers_v2 or not item.offers_v2.listings:
continue
listing = item.offers_v2.listings[0]
if not listing.price or not listing.price.money:
continue
price = float(listing.price.money.amount)
list_price = None
savings_pct = 0
savings_eur = 0
if listing.price.savings and listing.price.savings.money:
savings_eur = float(listing.price.savings.money.amount)
savings_pct = int(listing.price.savings.percentage)
if listing.price.saving_basis and listing.price.saving_basis.money:
list_price = float(listing.price.saving_basis.money.amount)
# Niedrigere Schwelle für Kinderspiele (auch kleine Rabatte)
if savings_pct > 0:
if list_price is None:
list_price = price
deals.append({
"title": title, "image": image, "asin": asin,
"price": price, "list_price": list_price,
"savings_pct": savings_pct, "savings_eur": savings_eur,
"url": f"https://www.amazon.de/dp/{asin}?tag={PARTNER_TAG}",
})
except Exception:
continue
return deals
def guess_age_group(title):
"""Guess age group from title."""
tl = title.lower()
if re.search(r'ab\s+3\s+jahr', tl):
return "3+"
if re.search(r'ab\s+4\s+jahr', tl):
return "4+"
if re.search(r'ab\s+5\s+jahr', tl):
return "5+"
if re.search(r'ab\s+6\s+jahr', tl):
return "6+"
if re.search(r'ab\s+7\s+jahr', tl):
return "7+"
if 'mitbringspiel' in tl:
return "Mitbringspiele"
if re.search(r'ab\s+8\s+jahr', tl):
return "8+"
# Fallback: check for junior/kids markers
if 'junior' in tl or 'kids' in tl:
return "Mitbringspiele"
return "Allgemein"
AGE_ORDER = ["3+", "4+", "5+", "6+", "7+", "8+", "Mitbringspiele", "Allgemein"]
def generate_html(deals, output_path):
seen = set()
unique = []
for d in deals:
if d["asin"] not in seen:
seen.add(d["asin"])
unique.append(d)
unique.sort(key=lambda x: x["savings_pct"], reverse=True)
top = unique[:20]
# Group by age
groups = {}
for d in top:
age = guess_age_group(d["title"])
if age not in groups:
groups[age] = []
groups[age].append(d)
now = datetime.now().strftime("%d.%m.%Y, %H:%M")
sections = []
for age in AGE_ORDER:
if age not in groups:
continue
items = groups[age]
age_labels = {
"3+": "Für die Kleinsten (ab 3 Jahre)",
"4+": "Ab 4 Jahre",
"5+": "Ab 5 Jahre",
"6+": "Ab 6 Jahre",
"7+": "Ab 7 Jahre",
"8+": "Ab 8 Jahre",
"Mitbringspiele": "Mitbringspiele",
"Allgemein": "Weitere Kinderspiele",
}
label = age_labels.get(age, age)
cards = []
for d in items:
cards.append(f"""<div style="display:flex;gap:1rem;padding:1rem 0;border-bottom:1px solid #eee;">
<img src="{d['image']}" alt="" style="width:100px;height:100px;object-fit:contain;" loading="lazy">
<div style="flex:1;"><strong>{d['title'][:120]}</strong><br>
<span style="color:#b12704;font-size:1.2rem;font-weight:bold;">{d['price']:.2f} €</span>
<span style="text-decoration:line-through;color:#999;margin-left:0.5rem;">UVP {d['list_price']:.2f} €</span>
<span style="background:#b12704;color:#fff;padding:2px 6px;border-radius:3px;margin-left:0.5rem;font-size:0.85rem;">-{d['savings_pct']}%</span>
<span style="color:#b12704;margin-left:0.3rem;">({d['savings_eur']:.2f} € gespart)</span><br>
<a href="{d['url']}" target="_blank" rel="noopener" style="color:#007185;">Zu Amazon →</a></div></div>""")
sections.append(f"<h2>{label}</h2>\n{''.join(cards)}")
count_info = f"{len(top)} Spiele in {len(groups)} Altersgruppen"
html = f"""<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Kinderspiele-Angebote auf Amazon</title>
<style>body{{margin:0;padding:0;background:#f9f7f4;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#2c2416;line-height:1.6}}
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;color:#1a1a1a}}h2{{font-size:1.2rem;color:#20228a;margin-top:2rem;padding-bottom:0.5rem;border-bottom:2px solid #20228a}}
.meta{{color:#999;font-size:.85rem;margin-bottom:1.5rem}}
a{{color:#007185;text-decoration:none}}a:hover{{text-decoration:underline}}</style></head><body><article>
<h1>Kinderspiele-Angebote auf Amazon</h1><p class="meta">Stand: {now} · {count_info} · Preise inkl. MwSt.</p>
{''.join(sections) if sections else '<p>Keine reduzierten Kinderspiele gefunden.</p>'}
<p class="meta" style="margin-top:2rem;">Preise und Verfügbarkeit können sich schnell ändern. Über die Links ({PARTNER_TAG}) kauft ihr zum gleichen Preis — Amazon zahlt eine kleine Provision.</p>
<p style="font-size:0.8rem;color:#999;text-align:center;margin-top:1rem;">Brettspiel-News.de · (C) liegt beim jeweiligen Rechteinhaber</p>
</article></body></html>"""
with open(output_path, 'w') as f:
f.write(html)
return len(top), groups
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--limit', type=int, default=20)
parser.add_argument('--output', type=str, default=None)
args = parser.parse_args()
if args.output is None:
args.output = os.path.expanduser(f"~/workspace/kinderspiele_deals_{datetime.now().strftime('%Y-%m-%d')}.html")
api = AmazonCreatorsApi(
CREDENTIAL_ID, CREDENTIAL_SECRET, "3.2", PARTNER_TAG,
country=COUNTRY, throttling=1.0
)
print(f"Connected to CreatorAPI (DE) — Kinderspiele-Suche")
print(f"Keywords: {len(KINDERSPIEL_KEYWORDS)} · {MAX_PAGES} Seiten à {ITEMS_PER_PAGE}\n")
all_deals = []
for kw in KINDERSPIEL_KEYWORDS:
for page in range(1, MAX_PAGES + 1):
resp = search_amazon(api, kw, page)
if resp:
deals = extract_deals(resp)
all_deals.extend(deals)
total = resp.total_result_count if hasattr(resp, 'total_result_count') else '?'
if deals:
names = [d['title'][:50] for d in deals]
print(f" {kw} p{page}: {len(deals)} Kinderspiele (total: {total})")
for n in names:
print(f"{n}")
else:
print(f" {kw} p{page}: 0 Kinderspiele (total: {total})")
# Early exit if we have enough
if len(all_deals) >= args.limit * 2:
break
count, groups = generate_html(all_deals, args.output)
print(f"\n{count} reduzierte Kinderspiele → {args.output}")
print(f"Link: http://185.162.249.159:8890/{os.path.basename(args.output)}?key=br3ttsp1el-n3ws-2026")
# Print summary for Joomla article building
print(f"\n=== SUMMARY ===")
print(f"Total: {count} Spiele")
for age in AGE_ORDER:
if age in groups:
print(f" {age}: {len(groups[age])} Spiele")
for d in groups[age]:
print(f" {d['savings_pct']}% | {d['price']:.2f}€ | {d['title'][:80]} | ASIN:{d['asin']}")
if __name__ == "__main__":
main()