310 lines
12 KiB
Python
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()
|