241 lines
9.6 KiB
Python
241 lines
9.6 KiB
Python
#!/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"""<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;">
|
|
<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" style="color:#007185;">Zu Amazon →</a></div></div>""")
|
|
|
|
html = f"""<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
<title>Brettspiel-Angebote — Amazon Deals</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}}.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>Brettspiel-Angebote auf Amazon</h1><p class="meta">Stand: {now} · Mindestens 10 % Rabatt · Preise inkl. MwSt. · {len(unique)} Deals gefunden (Top 50)</p>
|
|
{''.join(cards) if cards else '<p>Keine reduzierten Brettspiele 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>
|
|
</article></body></html>"""
|
|
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()
|