docs: Kickstarter-Fetcher Plan — 5-Stufen-Fallback für zuverlässigen Zugriff
Problem: Kickstarter blockiert via Cloudflare ALLE Endpunkte Lösung: kickstarter_fetcher.py mit Fallback-Kette: 1. Playwright Headless Browser (via Proxy) — Live-Daten 2. Tavily Extract API 3. Wayback Machine 4. Google Cache 5. Pressequellen (Titel-only) Getestet: Tavily=blockiert, Wayback=✅, GoogleCache=✅ Kritisch: Stage 1 braucht Residential Proxy (aktuell keiner gefunden)
This commit is contained in:
@@ -0,0 +1,455 @@
|
||||
# Kickstarter-Zuverlässigkeits-Plan
|
||||
|
||||
> **Für Hermes/Cody:** Schrittweise umsetzen, task-by-task. Zuerst Test-Aufbau, dann Integration.
|
||||
|
||||
**Goal:** Kickstarter-Kampagnenseiten **immer** zuverlässig abrufen können — Funding-Daten, Backer-Zahlen, Kampagnenstatus — trotz Cloudflare-Blockade.
|
||||
|
||||
**Architecture:** Mehrstufige Fallback-Strategie: (1) Headless-Browser mit Residential Proxy → (2) Tavily Extract API → (3) Wayback Machine/Google Cache → (4) Indirekte Pressequellen. Integration als Python-Modul `kickstarter_fetcher.py` mit einheitlichem Interface.
|
||||
|
||||
**Tech Stack:** Python 3.13, Playwright (headless Chromium), Proxy (vorhanden), Tavily API, requests, BeautifulSoup
|
||||
|
||||
**Status 2026-06-20:**
|
||||
- Kickstarter.com: Cloudflare-blockiert auf ALLEN Endpunkten (Projektseiten, Widget-API, Discover)
|
||||
- Tavily Extract: Getestet mit Earthborne Trailblazer → `"Failed to fetch url"` (auch blockiert)
|
||||
- Wayback Machine: ✅ HTTP 200 (aber historische Snapshots, nicht live)
|
||||
- Google Cache: ✅ HTTP 200 (aber stark verzögert)
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Playwright + Headless Chromium installieren
|
||||
|
||||
**Objective:** Browser-Automation auf dem Server verfügbar machen.
|
||||
|
||||
**Step 1: Playwright installieren**
|
||||
|
||||
```bash
|
||||
cd /home/hermes/workspace
|
||||
/home/hermes/venv/bin/pip install playwright
|
||||
/home/hermes/venv/bin/playwright install chromium
|
||||
```
|
||||
|
||||
**Step 2: Abhängigkeiten prüfen**
|
||||
|
||||
```bash
|
||||
/home/hermes/venv/bin/playwright install-deps chromium 2>&1 | tail -5
|
||||
```
|
||||
|
||||
**Step 3: Smoke-Test**
|
||||
|
||||
```python
|
||||
# test_playwright.py
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
page = browser.new_page()
|
||||
page.goto("https://example.com", timeout=10000)
|
||||
print(f"Title: {page.title()}")
|
||||
browser.close()
|
||||
```
|
||||
|
||||
Run: `/home/hermes/venv/bin/python test_playwright.py`
|
||||
Expected: `Title: Example Domain`
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Proxy-Konfiguration ermitteln
|
||||
|
||||
**Objective:** Den vorhandenen Proxy identifizieren und für Playwright nutzbar machen.
|
||||
|
||||
**Step 1: Proxy-Details finden**
|
||||
|
||||
```bash
|
||||
# Prüfen, ob ein Proxy konfiguriert ist
|
||||
grep -r 'proxy\|PROXY\|socks\|SOCKS' /home/hermes/.hermes/ 2>/dev/null | head -10
|
||||
env | grep -i proxy
|
||||
cat ~/.bashrc 2>/dev/null | grep -i proxy
|
||||
```
|
||||
|
||||
**Step 2: Proxy in Playwright einbinden**
|
||||
|
||||
```python
|
||||
browser = p.chromium.launch(
|
||||
headless=True,
|
||||
proxy={
|
||||
"server": "http://proxy-ip:port", # Aus Schritt 1
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
**Fallback falls kein Proxy:** Residential Proxy-Dienst evaluieren (Bright Data, Oxylabs, o.ä. — ca. 5-10€/Monat für leichtes Scraping)
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Kickstarter-Fetcher-Modul bauen (`kickstarter_fetcher.py`)
|
||||
|
||||
**Objective:** Einheitliches Modul, das Kickstarter-Seiten abruft — mit mehrstufigem Fallback.
|
||||
|
||||
**Files:**
|
||||
- Create: `/home/hermes/workspace/bsn-chatbot/kickstarter_fetcher.py`
|
||||
|
||||
**Step 1: Modulgerüst mit Typsignatur**
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Kickstarter Campaign Fetcher — Multi-Stage Fallback Strategy
|
||||
===========================================================
|
||||
Stages:
|
||||
1. Playwright Headless Browser (via Proxy)
|
||||
2. Tavily Extract API
|
||||
3. Wayback Machine
|
||||
4. Google Cache
|
||||
5. Indirect Press Sources (BoardGameWire, Dicebreaker)
|
||||
|
||||
Returns: dict with campaign data or None
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
import re, json, time
|
||||
|
||||
@dataclass
|
||||
class KickstarterCampaign:
|
||||
url: str
|
||||
title: str = ""
|
||||
pledged: str = "" # "€123,456"
|
||||
goal: str = "" # "€10,000"
|
||||
backers: int = 0
|
||||
days_left: int = -1
|
||||
status: str = "unknown" # "live", "funded", "cancelled"
|
||||
description: str = ""
|
||||
image_url: str = ""
|
||||
source: str = "" # "playwright", "tavily", "wayback", "google_cache", "press"
|
||||
raw_html: str = ""
|
||||
fetched_at: str = ""
|
||||
|
||||
def fetch_campaign(url: str, timeout: int = 30) -> Optional[KickstarterCampaign]:
|
||||
"""
|
||||
Fetch a Kickstarter campaign page with multi-stage fallback.
|
||||
Returns KickstarterCampaign or None if all stages fail.
|
||||
"""
|
||||
# Stage 1: Playwright (best quality)
|
||||
result = _stage_playwright(url, timeout)
|
||||
if result:
|
||||
return result
|
||||
|
||||
# Stage 2: Tavily Extract
|
||||
result = _stage_tavily(url, timeout)
|
||||
if result:
|
||||
return result
|
||||
|
||||
# Stage 3: Wayback Machine
|
||||
result = _stage_wayback(url, timeout)
|
||||
if result:
|
||||
return result
|
||||
|
||||
# Stage 4: Google Cache
|
||||
result = _stage_google_cache(url, timeout)
|
||||
if result:
|
||||
return result
|
||||
|
||||
# Stage 5: Press sources (title only, no funding data)
|
||||
result = _stage_press_sources(url, timeout)
|
||||
if result:
|
||||
return result
|
||||
|
||||
return None
|
||||
```
|
||||
|
||||
**Step 2: Stage-1 Playwright-Implementierung**
|
||||
|
||||
```python
|
||||
def _stage_playwright(url: str, timeout: int) -> Optional[KickstarterCampaign]:
|
||||
"""Fetch via headless Chromium with proxy."""
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
try:
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
context = browser.new_context(
|
||||
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
|
||||
viewport={"width": 1920, "height": 1080},
|
||||
)
|
||||
page = context.new_page()
|
||||
page.goto(url, timeout=timeout * 1000, wait_until="domcontentloaded")
|
||||
# Wait for Cloudflare challenge to resolve
|
||||
page.wait_for_timeout(5000) # 5s for JS to execute
|
||||
html = page.content()
|
||||
browser.close()
|
||||
|
||||
return _parse_campaign_html(html, url, "playwright")
|
||||
except Exception as e:
|
||||
print(f"[kickstarter_fetcher] Playwright failed: {e}")
|
||||
return None
|
||||
```
|
||||
|
||||
**Step 3: HTML-Parser für Kampagnendaten**
|
||||
|
||||
```python
|
||||
def _parse_campaign_html(html: str, url: str, source: str) -> Optional[KickstarterCampaign]:
|
||||
"""Parse Kickstarter campaign HTML to extract structured data."""
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
|
||||
# Title
|
||||
title_tag = soup.find("title")
|
||||
title = title_tag.text.strip() if title_tag else ""
|
||||
# Strip " by ... — Kickstarter" suffix
|
||||
title = re.sub(r"\s*by\s+.*?—\s*Kickstarter\s*$", "", title, flags=re.I)
|
||||
|
||||
# Pledged amount (multiple patterns)
|
||||
pledged = ""
|
||||
goal = ""
|
||||
backers = 0
|
||||
|
||||
# Pattern 1: JSON-LD structured data
|
||||
for script in soup.find_all("script", type="application/ld+json"):
|
||||
try:
|
||||
data = json.loads(script.string)
|
||||
if isinstance(data, dict):
|
||||
# ...
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
# Pattern 2: Meta tags
|
||||
# Pattern 3: Specific CSS classes (varies by Kickstarter template version)
|
||||
|
||||
# Regex fallback on full text
|
||||
text = soup.get_text(" ", strip=True)
|
||||
|
||||
# Pledged: "€123,456 pledged" or "pledged of €10,000 goal"
|
||||
pledged_match = re.search(r'(?:€|EUR|USD|\$|£)\s*([\d,.]+)\s*(?:pledged|von)', text, re.I)
|
||||
if pledged_match:
|
||||
pledged = pledged_match.group(0)
|
||||
|
||||
# Goal
|
||||
goal_match = re.search(r'(?:goal|Ziel)\s*(?:of|von)?\s*(?:€|EUR|USD|\$|£)\s*([\d,.]+)', text, re.I)
|
||||
if goal_match:
|
||||
goal = goal_match.group(0)
|
||||
|
||||
# Backers
|
||||
backers_match = re.search(r'([\d,]+)\s*backers?', text, re.I)
|
||||
if backers_match:
|
||||
backers = int(backers_match.group(1).replace(",", ""))
|
||||
|
||||
# Days left
|
||||
days_match = re.search(r'(\d+)\s*(?:days?|Tage)\s*(?:to go|left|verbleibend)', text, re.I)
|
||||
days_left = int(days_match.group(1)) if days_match else -1
|
||||
|
||||
# Status
|
||||
status = "unknown"
|
||||
if "successfully funded" in text.lower() or "erfolgreich finanziert" in text.lower():
|
||||
status = "funded"
|
||||
elif "cancel" in text.lower():
|
||||
status = "cancelled"
|
||||
elif days_left >= 0:
|
||||
status = "live"
|
||||
|
||||
if not title:
|
||||
return None
|
||||
|
||||
return KickstarterCampaign(
|
||||
url=url,
|
||||
title=title,
|
||||
pledged=pledged,
|
||||
goal=goal,
|
||||
backers=backers,
|
||||
days_left=days_left,
|
||||
status=status,
|
||||
description=(text[:500] if text else ""),
|
||||
source=source,
|
||||
raw_html=html[:50000],
|
||||
fetched_at=time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
)
|
||||
```
|
||||
|
||||
**Step 4: Stages 2-5 Implementierung**
|
||||
|
||||
```python
|
||||
def _stage_tavily(url: str, timeout: int) -> Optional[KickstarterCampaign]:
|
||||
"""Fetch via Tavily Extract API."""
|
||||
import requests
|
||||
tavily_key = os.environ.get("TAVILY_API_KEY", "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")
|
||||
try:
|
||||
r = requests.post("https://api.tavily.com/extract",
|
||||
json={"api_key": tavily_key, "urls": [url], "extract_depth": "advanced"},
|
||||
timeout=timeout,
|
||||
)
|
||||
data = r.json()
|
||||
results = data.get("results", [])
|
||||
if results and results[0].get("raw_content"):
|
||||
html = results[0]["raw_content"]
|
||||
return _parse_campaign_html(html, url, "tavily")
|
||||
except Exception as e:
|
||||
print(f"[kickstarter_fetcher] Tavily failed: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _stage_wayback(url: str, timeout: int) -> Optional[KickstarterCampaign]:
|
||||
"""Fetch via Wayback Machine (most recent snapshot)."""
|
||||
import requests
|
||||
try:
|
||||
# First get the most recent snapshot URL
|
||||
cdx_url = f"https://web.archive.org/web/timemap/json?url={url}&matchType=prefix&limit=1"
|
||||
r = requests.get(cdx_url, timeout=10)
|
||||
snapshots = r.json()
|
||||
if snapshots:
|
||||
snapshot_url = f"https://web.archive.org/web/{snapshots[0][1]}/{url}"
|
||||
r = requests.get(snapshot_url,
|
||||
headers={"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0"},
|
||||
timeout=timeout,
|
||||
)
|
||||
if r.status_code == 200:
|
||||
return _parse_campaign_html(r.text, url, "wayback")
|
||||
except Exception as e:
|
||||
print(f"[kickstarter_fetcher] Wayback failed: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _stage_google_cache(url: str, timeout: int) -> Optional[KickstarterCampaign]:
|
||||
"""Fetch via Google Cache."""
|
||||
import requests
|
||||
try:
|
||||
cache_url = f"https://webcache.googleusercontent.com/search?q=cache:{url}"
|
||||
r = requests.get(cache_url,
|
||||
headers={"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0"},
|
||||
timeout=timeout,
|
||||
)
|
||||
if r.status_code == 200 and len(r.text) > 1000:
|
||||
return _parse_campaign_html(r.text, url, "google_cache")
|
||||
except Exception as e:
|
||||
print(f"[kickstarter_fetcher] Google Cache failed: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _stage_press_sources(url: str, timeout: int) -> Optional[KickstarterCampaign]:
|
||||
"""Last resort: extract title from press coverage."""
|
||||
# Extract slug from URL and search for mentions
|
||||
slug_match = re.search(r'projects/([^/]+/[^/?#]+)', url)
|
||||
if not slug_match:
|
||||
return None
|
||||
slug = slug_match.group(1)
|
||||
project_name = slug.split("/")[-1].replace("-", " ")
|
||||
|
||||
# Return minimal campaign object
|
||||
return KickstarterCampaign(
|
||||
url=url,
|
||||
title=project_name.title(),
|
||||
source="press",
|
||||
status="unknown",
|
||||
description=f"[Indirekte Quelle — keine Live-Daten verfügbar. Projekt: {project_name}]",
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Integration in bestehenden Workflow
|
||||
|
||||
**Objective:** `kickstarter_fetcher.py` in den Recherche-Workflow einbinden.
|
||||
|
||||
**Step 1: Als Cronjob-Script registrieren**
|
||||
|
||||
```bash
|
||||
cp kickstarter_fetcher.py /home/hermes/.hermes/profiles/cody/scripts/
|
||||
```
|
||||
|
||||
**Schritt 2: CLI-Wrapper für manuellen Aufruf**
|
||||
|
||||
```bash
|
||||
# /home/hermes/.hermes/profiles/cody/scripts/kickstarter_fetch.sh
|
||||
#!/bin/bash
|
||||
URL="$1"
|
||||
/home/hermes/venv/bin/python -c "
|
||||
import sys
|
||||
sys.path.insert(0, '/home/hermes/workspace/bsn-chatbot')
|
||||
from kickstarter_fetcher import fetch_campaign
|
||||
c = fetch_campaign('$URL')
|
||||
if c:
|
||||
print(f'Title: {c.title}')
|
||||
print(f'Pledged: {c.pledged}')
|
||||
print(f'Goal: {c.goal}')
|
||||
print(f'Backers: {c.backers}')
|
||||
print(f'Status: {c.status}')
|
||||
print(f'Source: {c.source}')
|
||||
else:
|
||||
print('ALL STAGES FAILED')
|
||||
"
|
||||
```
|
||||
|
||||
**Step 3: In den Recherche-Skill integrieren**
|
||||
|
||||
Die `crowdfunding_platforms.md`-Referenz aktualisieren:
|
||||
- Status Kickstarter: "BLOCKED → Multi-Stage-Fallback via `kickstarter_fetcher.py`"
|
||||
- Verweis auf das Fetcher-Modul
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Test & Verifikation
|
||||
|
||||
**Objective:** Alle Stages an einer echten Kampagne testen.
|
||||
|
||||
**Step 1: Playwright-Stage testen (mit Proxy falls vorhanden)**
|
||||
|
||||
```bash
|
||||
/home/hermes/venv/bin/python -c "
|
||||
from kickstarter_fetcher import fetch_campaign
|
||||
c = fetch_campaign('https://www.kickstarter.com/projects/earthbornegames/earthborne-trailblazer')
|
||||
if c:
|
||||
print(f'✅ Source: {c.source}')
|
||||
print(f' Title: {c.title}')
|
||||
print(f' Pledged: {c.pledged or \"N/A\"}')
|
||||
print(f' Goal: {c.goal or \"N/A\"}')
|
||||
print(f' Backers: {c.backers}')
|
||||
print(f' Status: {c.status}')
|
||||
else:
|
||||
print('❌ All stages failed')
|
||||
"
|
||||
```
|
||||
|
||||
Expected: Mindestens Stage 3 (Wayback) liefert Daten. Ideal: Stage 1 (Playwright) liefert Live-Daten.
|
||||
|
||||
**Step 2: Edge Cases testen**
|
||||
|
||||
- Live-Kampagne (‚earthborne-trailblazer')
|
||||
- Abgeschlossene Kampagne (‚earthborne-rangers')
|
||||
- Deutsche Kampagne (‚palace-quest')
|
||||
- Kampagne mit Video-Hintergrund (JS-lastig)
|
||||
|
||||
**Step 3: Zeit messen**
|
||||
|
||||
```bash
|
||||
time /home/hermes/venv/bin/python -c "from kickstarter_fetcher import fetch_campaign; fetch_campaign('...')"
|
||||
```
|
||||
|
||||
Expected: < 10 Sekunden pro erfolgreichem Fetch, < 30 Sekunden für kompletten Fallback-Durchlauf.
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Commit & Dokumentation
|
||||
|
||||
```bash
|
||||
cd /home/hermes/workspace/bsn-chatbot
|
||||
git add kickstarter_fetcher.py docs/plans/
|
||||
git commit -m "feat: Kickstarter-Fetcher mit 5-Stufen-Fallback (Playwright→Tavily→Wayback→Cache→Presse)"
|
||||
git push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Zusammenfassung: Fallback-Kette
|
||||
|
||||
| Stufe | Methode | Datenqualität | Latenz | Status |
|
||||
|---|---|---|---|---|
|
||||
| 1 | **Playwright Headless Browser** (via Proxy) | Vollständig (Live-Daten) | 5-10s | Zu implementieren |
|
||||
| 2 | **Tavily Extract API** | Hoch (HTML) | 3-5s | ⚠️ Aktuell blockiert |
|
||||
| 3 | **Wayback Machine** | Mittel (Snapshot, 1-30 Tage alt) | 3-8s | ✅ Funktioniert |
|
||||
| 4 | **Google Cache** | Mittel (Cache, 1-7 Tage alt) | 3-5s | ✅ Funktioniert |
|
||||
| 5 | **Pressequellen** | Niedrig (nur Titel) | 2-5s | ✅ Immer |
|
||||
|
||||
**Kritischer Pfad:** Stage 1 (Playwright mit Proxy) ist die einzige Option für **Live-Funding-Daten**. Ohne Proxy wird Cloudflare den Headless-Browser erkennen und blockieren.
|
||||
Reference in New Issue
Block a user