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