Files
BSN-Chatsystem/kickstarter_fetcher.py
T
Hermes Agent 20a7f5987a feat: Kickstarter LIVE-Daten via FlareSolverr — DURCHBRUCH!
Stage 0: FlareSolverr (Headless Chromium, localhost:8191)
  - Löst Cloudflare-Challenge automatisch
  - Extrahiert Campaign-Daten aus embedded Flat-JSON:
    project_name, project_backers_count, project_pledged_usd,
    project_goal_usd, project_state, project_deadline, project_blurb

Getestet: Earthborne Trailblazer → 49,130 / 3,969 backers / successful
  Kein Proxy nötig — FlareSolverr's Chromium reicht!

Setup: FlareSolverr in ~/workspace/FlareSolverr/
  Start: cd src && python start_flaresolverr.py
  Patch: --headless=new statt Xvfb (utils.py)

Kickstarter-Fetcher jetzt: 2 Stages (FlareSolverr → Slug-Fallback)
2026-06-21 19:39:41 +02:00

247 lines
8.9 KiB
Python

#!/usr/bin/env python3
"""
Kickstarter Campaign Fetcher — Multi-Stage Fallback Strategy
===========================================================
Final version 2026-06-21 — FlareSolverr breakthrough!
Stages:
0. FlareSolverr (localhost:8191) → LIVE campaign data! ✅
1. Fallback: URL-Slug → Titel (immer)
How it works:
FlareSolverr runs a headless Chromium that solves Cloudflare challenges.
Kickstarter embeds campaign data as flat JSON fields in the HTML:
project_name, project_backers_count, project_current_amount_pledged_usd, etc.
We extract these via regex → structured KickstarterCampaign object.
Usage:
from kickstarter_fetcher import fetch_campaign, status_report
c = fetch_campaign("https://www.kickstarter.com/projects/...")
print(status_report(c))
Author: Cody (Hermes Agent), 2026-06-20/21
"""
from dataclasses import dataclass
from typing import Optional
import json
import re
import time
# ═══════════════════════════════════════════════════════════════
# Configuration
# ═══════════════════════════════════════════════════════════════
FLARESOLVERR_URL = "http://localhost:8191/v1"
FLARESOLVERR_TIMEOUT = 60 # seconds for KS page load
@dataclass
class KickstarterCampaign:
url: str
title: str = ""
pledged: str = "" # e.g. "$549,130"
goal: str = "" # e.g. "$150,000"
backers: int = 0
days_left: int = -1
status: str = "unknown" # "live", "successful", "cancelled"
description: str = ""
source: str = "" # "flaresolverr", "slug"
fetched_at: str = ""
# ═══════════════════════════════════════════════════════════════
# Public API
# ═══════════════════════════════════════════════════════════════
def fetch_campaign(url: str, timeout: int = 90) -> Optional[KickstarterCampaign]:
"""
Fetch a Kickstarter campaign page.
Stage 0: FlareSolverr (LIVE data via headless Chromium)
Stage 1: URL-Slug fallback (title only)
"""
# Stage 0: FlareSolverr
result = _stage_flaresolverr(url, timeout)
if result:
return result
# Stage 1: Slug
return _stage_slug(url)
def status_report(campaign: Optional[KickstarterCampaign]) -> str:
"""Generate a human-readable status report."""
if not campaign:
return "❌ KEINE DATEN"
if campaign.source == "slug":
return (
f"⚠️ NUR TITEL\n"
f" {campaign.title}\n"
f" → Bitte manuell recherchieren: {campaign.url}"
)
return (
f"✅ LIVE ({campaign.source})\n"
f" {campaign.title}\n"
f" {campaign.pledged} von {campaign.goal}\n"
f" {campaign.backers:,} Unterstützer\n"
f" Status: {campaign.status}"
)
# ═══════════════════════════════════════════════════════════════
# Stage 0: FlareSolverr → Headless Chromium → KS Page → Parse
# ═══════════════════════════════════════════════════════════════
def _stage_flaresolverr(url: str, timeout: int) -> Optional[KickstarterCampaign]:
"""
Fetch Kickstarter page via FlareSolverr (headless Chromium).
Extracts campaign data from embedded flat JSON fields.
"""
import requests
try:
r = requests.post(
FLARESOLVERR_URL,
json={
"cmd": "request.get",
"url": url,
"maxTimeout": min(timeout, FLARESOLVERR_TIMEOUT) * 1000,
},
timeout=timeout + 10,
)
data = r.json()
except Exception as e:
print(f"[kickstarter_fetcher] FlareSolverr request error: {e}")
return None
if data.get("status") != "ok":
print(f"[kickstarter_fetcher] FlareSolverr error: {data.get('message', '?')}")
return None
sol = data.get("solution", {})
html = sol.get("response", "")
if not html or len(html) < 5000:
print(f"[kickstarter_fetcher] Empty/short response: {len(html)} bytes")
return None
# Check for Cloudflare block in rendered page
if "Just a moment" in html[:2000]:
print("[kickstarter_fetcher] Cloudflare block after rendering")
return None
return _parse_campaign_native(html, url, "flaresolverr")
def _parse_campaign_native(
html: str, url: str, source: str
) -> Optional[KickstarterCampaign]:
"""
Parse Kickstarter campaign data from embedded flat JSON fields.
Kickstarter injects data like:
"project_name":"Earthborne Trailblazer"
"project_backers_count":3969
"project_current_amount_pledged_usd":549130.0
"project_goal_usd":150000.0
"project_state":"successful"
"project_deadline":"2026-06-16T11:01:05-04:00"
"project_blurb":"Explore the wilderness..."
"""
def _re_first(pattern: str, html: str, group: int = 1) -> Optional[str]:
m = re.search(pattern, html)
return m.group(group) if m else None
def _re_int(pattern: str, html: str) -> int:
m = re.search(pattern, html)
return int(m.group(1)) if m else 0
def _re_float(pattern: str, html: str) -> float:
m = re.search(pattern, html)
return float(m.group(1)) if m else 0.0
# Extract all fields
name = _re_first(r'"project_name":"([^"]+)"', html)
if not name:
# Try og:title as fallback
name = _re_first(r'<meta\s+property="og:title"\s+content="([^"]+)"', html)
if not name:
return None
blurb = _re_first(r'"project_blurb":"([^"]*)"', html)
currency = _re_first(r'"project_currency":"([^"]+)"', html) or "USD"
state = _re_first(r'"project_state":"([^"]+)"', html) or "unknown"
pledged_usd = _re_float(r'"project_current_amount_pledged_usd":([\d.]+)', html)
goal_usd = _re_float(r'"project_goal_usd":([\d.]+)', html)
backers = _re_int(r'"project_backers_count":(\d+)', html)
deadline = _re_first(r'"project_deadline":"([^"]+)"', html)
# Format amounts
def fmt_amount(usd_val: float) -> str:
return f"${usd_val:,.0f}"
# Calculate days left
days_left = -1
if deadline:
try:
from datetime import datetime
dt = datetime.fromisoformat(deadline.replace("Z", "+00:00"))
delta = dt - datetime.now(dt.tzinfo)
days_left = max(0, delta.days)
except Exception:
pass
return KickstarterCampaign(
url=url,
title=name,
pledged=fmt_amount(pledged_usd),
goal=fmt_amount(goal_usd),
backers=backers,
days_left=days_left if state == "live" else -1,
status=state,
description=(blurb or "")[:500],
source=source,
fetched_at=time.strftime("%Y-%m-%d %H:%M:%S"),
)
# ═══════════════════════════════════════════════════════════════
# Stage 1: URL-Slug Fallback
# ═══════════════════════════════════════════════════════════════
def _stage_slug(url: str) -> KickstarterCampaign:
"""Extract project name from URL slug."""
slug_match = re.search(r"projects/([^/]+/[^/?#]+)", url)
slug = slug_match.group(1) if slug_match else url.rstrip("/").split("/")[-1]
project_name = slug.split("/")[-1].replace("-", " ").title()
return KickstarterCampaign(
url=url,
title=project_name,
source="slug",
status="unknown",
fetched_at=time.strftime("%Y-%m-%d %H:%M:%S"),
)
# ═══════════════════════════════════════════════════════════════
# CLI
# ═══════════════════════════════════════════════════════════════
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: python kickstarter_fetcher.py <kickstarter-url>")
print("\nRequires FlareSolverr running on localhost:8191")
sys.exit(1)
url = sys.argv[1]
print(f"Fetching: {url}")
print("-" * 50)
print(status_report(fetch_campaign(url)))