#!/usr/bin/env python3 """Amazon CreatorAPI 3.2 — Brettspiel-Deals auf amazon.de finden (verbessert). Änderungen: - item_count=50 (max), MAX_PAGES=10 (max) - 15 Keywords statt 5 — deckt mehr Spieltypen ab - Ohne min_saving_percent (Client-seitiger Filter, zuverlässiger) - Zusätzlich: keyword-lose Browse-Node-Suche für alle Deals """ import argparse, os, re, time from datetime import datetime from amazon_creatorsapi import AmazonCreatorsApi, Country from amazon_creatorsapi import models as m # Configuration CREDENTIAL_ID = "amzn1.application-oa2-client.91799968d0744e66950ea413659197d4" 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 = 10 ITEMS_PER_PAGE = 50 # Breite Keyword-Abdeckung — 15 verschiedene Suchbegriffe KEYWORDS = [ "Brettspiel", "Kartenspiel", "Familienspiel", "Strategiespiel", "Partyspiel", "Kennerspiel", "Kinderspiel", "Würfelspiel", "Legespiel", "kooperativ", "Roll & Write", "Deckbuilding", "Worker Placement", "Drafting", "Puzzle", ] RESOURCES = [ m.SearchItemsResource.ITEM_INFO_DOT_TITLE, m.SearchItemsResource.IMAGES_DOT_PRIMARY_DOT_LARGE, m.SearchItemsResource.OFFERS_V2_DOT_LISTINGS_DOT_PRICE, ] def search_amazon(api, keyword=None, 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 or 'BROWSE'}' p{page}: {e}", flush=True) return None 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) 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) # Client-seitiger Filter — zuverlässiger als min_saving_percent if savings_pct >= 10: deals.append({ "title": title, "image": image, "asin": asin, "price": price, "list_price": list_price or 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 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) # === QUALITÄTSKONTROLLE: Nicht-Spiele herausfiltern === # Blacklist: Produkte, die definitiv KEINE Brettspiele sind NON_GAME_PATTERNS = [ # Elektronik & Gadgets r'\bAir\s*Tag\b', r'\bAirtag\b', r'\bGPS[-\s]Tracker\b', r'\bBluetooth[-\s]Tracker\b', r'\bKopfhörer\b', r'\bSmartwatch\b', # Software, digital, Steuer (NUR als eigenständiges Produkt) r'\bWISO\s+Steuer\b', r'\bSteuer\s*\d{4}\b', r'\bSteuererklärung\b', r'\bSteuersoftware\b', r'\bDigitaler\s+Download\b', r'\bAntivirus\b', r'\bAntiviren\b', # Bücher (explizit, nicht im Spielekontext) r'\bTaschenbuch\b', r'\bGebundene\s+Ausgabe\b', r'\bKindle\b', r'\bHörbuch\b', r'\bSchulbuch\b', r'\bLernheft\b', r'\bÜbungsbuch\b', r'\bVokabelheft\b', r'\bVokabeltrainer\b', # Medien (keine Spiele) r'\bBlu-ray\b', # Haushalt & Sonstiges r'\bNetzteil\b', r'\bLadegerät\b', # Sonstige Nicht-Spiele r'\bPolydraw\b', r'\bDrafting\s+Film\b', r'\bDrafting\s+Pad\b', r'\bBasic\s+Drafting\b', # Jigsaw-Puzzles (keine Brettspiele) r'\bPuzzle\s+\d+\s*Teile\b', r'\b\d+\s*Teile\s+Puzzle\b', r'\bJigsaw\s+Puzzle\b', r'\bPuzzle\s+\d+[.,]\d+\s*cm\b', ] filtered = [] removed = [] for d in unique: title_lower = d["title"].lower() is_non_game = False for pattern in NON_GAME_PATTERNS: if re.search(pattern, title_lower, re.IGNORECASE): is_non_game = True break if is_non_game: removed.append(d) else: filtered.append(d) if removed: print(f" QC: {len(removed)} Nicht-Spiele entfernt:") for r in removed: print(f" ✗ {r['title'][:80]}") unique = filtered # === ENDE QUALITÄTSKONTROLLE === unique.sort(key=lambda x: x["savings_pct"], reverse=True) top = unique[:50] # Show top 50 instead of 30 now = datetime.now().strftime("%d.%m.%Y, %H:%M") cards = [] for d in top: cards.append(f"""
{d['title'][:120]}
{d['price']:.2f} € UVP {d['list_price']:.2f} € -{d['savings_pct']}% ({d['savings_eur']:.2f} € gespart)
Zu Amazon →
""") html = f""" Brettspiel-Angebote — Amazon Deals

Brettspiel-Angebote auf Amazon

Stand: {now} · Mindestens 10 % Rabatt · Preise inkl. MwSt. · {len(unique)} Deals gefunden (Top 50)

{''.join(cards) if cards else '

Keine reduzierten Brettspiele gefunden.

'}

Preise und Verfügbarkeit können sich schnell ändern. Über die Links ({PARTNER_TAG}) kauft ihr zum gleichen Preis — Amazon zahlt eine kleine Provision.

""" with open(output_path, 'w') as f: f.write(html) return len(top) def main(): parser = argparse.ArgumentParser() parser.add_argument('--limit', type=int, default=50) parser.add_argument('--output', type=str, default=None) args = parser.parse_args() if args.output is None: args.output = os.path.expanduser(f"~/workspace/amazon_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) — {len(KEYWORDS)} Keywords, {ITEMS_PER_PAGE}/page, {MAX_PAGES} pages") all_deals = [] # 1. Keyword searches for kw in KEYWORDS: kw_deals = 0 for page in range(1, MAX_PAGES + 1): resp = search_amazon(api, kw, page) if not resp: break deals = extract_deals(resp) all_deals.extend(deals) kw_deals += len(deals) # Stop if fewer items returned (end of results) if resp.items and len(resp.items) < ITEMS_PER_PAGE: break print(f" '{kw}': {kw_deals} Deals gesamt") # 2. Browse ohne Keywords (catch-all) print(f" 'BROWSE' (keyword-frei): ", end="", flush=True) browse_deals = 0 for page in range(1, MAX_PAGES + 1): resp = search_amazon(api, keyword=None, page=page) if not resp: break deals = extract_deals(resp) all_deals.extend(deals) browse_deals += len(deals) if resp.items and len(resp.items) < ITEMS_PER_PAGE: break print(f"{browse_deals} Deals") count = generate_html(all_deals, args.output) print(f"\n{count} reduzierte Brettspiele (Top 50) → {args.output}") print(f"Link: http://185.162.249.159:8890/{os.path.basename(args.output)}?key=br3ttsp1el-n3ws-2026") if __name__ == "__main__": main()