#!/usr/bin/env python3 """ Tech-Trends Workflow (Hacker News + GitHub Trending) Täglich: Holt Top/New Stories von HN + GitHub Trending Repos, analysiert mit OpenRouter, postet deutschen Artikel mit Quellenlinks nach WordPress. """ import requests import base64 import logging import os import re from datetime import datetime, timedelta, timezone from pathlib import Path def _load_homelab_conf(): """Liest Schluessel-Wert-Paare aus homelab.conf.""" conf = {} for path in ["/opt/homelab-brain/homelab.conf", "/opt/homelab-ai-bot/../homelab.conf"]: p = Path(path) if p.exists(): for line in p.read_text().splitlines(): line = line.strip() if "=" in line and not line.startswith("#"): k, _, v = line.partition("=") conf[k.strip()] = v.strip().strip('"') break return conf _CONF = _load_homelab_conf() # ── Konfiguration ────────────────────────────────────────────── WP_URL = "https://arakava-news-2.orbitalo.net" WP_USER = "admin" WP_PASS = _CONF.get("PW_WP_ADMIN", "") OPENROUTER_KEY = _CONF.get("OPENROUTER_KEY", "") TELEGRAM_TOKEN = _CONF.get("TG_MUTTER_TOKEN", "") TELEGRAM_CHAT = "674951792" LOG_FILE = "/opt/rss-manager/logs/reddit_trends.log" WP_CATEGORY = "Tech-Trends" # ── Logging ──────────────────────────────────────────────────── os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True) logging.basicConfig( filename=LOG_FILE, level=logging.INFO, format="%(asctime)s [tech_trends] %(levelname)s %(message)s", ) log = logging.getLogger() # ── Hacker News API ──────────────────────────────────────────── def fetch_hn_stories(feed="topstories", limit=30, min_score=50): """Top/New/Best Stories von Hacker News holen.""" try: r = requests.get( f"https://hacker-news.firebaseio.com/v0/{feed}.json", timeout=10 ) ids = r.json()[:limit] stories = [] for story_id in ids: try: item = requests.get( f"https://hacker-news.firebaseio.com/v0/item/{story_id}.json", timeout=5 ).json() if not item or item.get("type") != "story": continue if item.get("score", 0) < min_score: continue # Nur Artikel der letzten 48h age_h = (datetime.now(timezone.utc).timestamp() - item.get("time", 0)) / 3600 if age_h > 48: continue stories.append({ "title": item.get("title", ""), "url": item.get("url", ""), "hn_url": f"https://news.ycombinator.com/item?id={story_id}", "score": item.get("score", 0), "comments": item.get("descendants", 0), "source": f"HN/{feed}", }) except Exception: continue log.info(f"HN/{feed}: {len(stories)} Stories (Score≥{min_score}, <48h)") return stories except Exception as e: log.warning(f"HN {feed} Fehler: {e}") return [] # ── GitHub Trending (Search API) ────────────────────────────── def fetch_github_trending(days=2, limit=10): """Repos die in den letzten X Tagen die meisten Stars bekamen.""" since = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d") try: r = requests.get( "https://api.github.com/search/repositories", params={ "q": f"created:>{since}", "sort": "stars", "order": "desc", "per_page": limit, }, headers={"Accept": "application/vnd.github.v3+json"}, timeout=15, ) if r.status_code != 200: log.warning(f"GitHub API: HTTP {r.status_code}") return [] repos = [] for repo in r.json().get("items", []): repos.append({ "title": f"{repo['full_name']}: {repo.get('description', '') or ''}", "url": repo["html_url"], "hn_url": repo["html_url"], "score": repo["stargazers_count"], "comments": repo.get("forks_count", 0), "source": "GitHub/Trending", "language": repo.get("language", ""), "topics": ", ".join(repo.get("topics", [])[:5]), }) log.info(f"GitHub Trending: {len(repos)} Repos (seit {since})") return repos except Exception as e: log.warning(f"GitHub Trending Fehler: {e}") return [] # ── OpenRouter Analyse ───────────────────────────────────────── def analyse_with_ki(hn_stories, gh_repos): hn_lines = [] for i, s in enumerate(hn_stories, 1): ext = f"\n 🔗 Artikel: {s['url']}" if s["url"] else "" hn_lines.append( f"{i}. [{s['source']}] {s['title']}" f"\n ⭐ Score: {s['score']} | 💬 Kommentare: {s['comments']}" f"\n 📎 HN: {s['hn_url']}{ext}" ) gh_lines = [] for i, r in enumerate(gh_repos, 1): lang = f" [{r['language']}]" if r.get("language") else "" topics = f" | Topics: {r['topics']}" if r.get("topics") else "" gh_lines.append( f"{i}. [GitHub]{lang} {r['title']}" f"\n ⭐ Stars: {r['score']}{topics}" f"\n 🔗 {r['url']}" ) prompt = f"""Du bist ein Tech-Journalist. Analysiere diese aktuellen Tech-Signale von Hacker News und GitHub und schreibe einen informativen deutschen Artikel für ein technikaffines Publikum. === HACKER NEWS (was Tech-Menschen gerade lesen & diskutieren) === {chr(10).join(hn_lines)} === GITHUB TRENDING (was gerade gebaut & gehypt wird) === {chr(10).join(gh_lines)} AUFGABE: 1. Identifiziere die 5-7 stärksten Trends — HN-Score und GitHub-Stars zeigen echten Hype 2. Schreibe einen strukturierten deutschen Artikel: - Knackige, neugierig machende Überschrift - Einleitung: 4-5 Sätze — was bewegt die Tech-Welt heute konkret - Pro Trend: H3-Überschrift + 4-5 Sätze mit echten Details aus den Quellen + Quellenlinks:
📰 Quelle lesen[GITHUB_LINK]
wobei [GITHUB_LINK] = " | ⭐ GitHub" wenn es ein Repo-Trend ist - Fazit: 3-4 Sätze Gesamtbild — wohin entwickelt sich die Branche 3. Mindestlänge: 25 Sätze, nutze konkrete Zahlen (Stars, Scores) als Belege 4. Stil: präzise, tiefgründig, kein Clickbait, auf Deutsch FORMAT: Nur HTML (h3, p, a). Kein Markdown, kein ```html.""" headers = { "Authorization": f"Bearer {OPENROUTER_KEY}", "Content-Type": "application/json", "HTTP-Referer": "https://arakava-news-2.orbitalo.net", "X-Title": "Arakava Tech Trends", } payload = { "model": "anthropic/claude-3.5-haiku", "messages": [{"role": "user", "content": prompt}], "max_tokens": 4000, "temperature": 0.7, } try: r = requests.post( "https://openrouter.ai/api/v1/chat/completions", headers=headers, json=payload, timeout=90, verify=False, ) if r.status_code == 200: content = r.json()["choices"][0]["message"]["content"].strip() content = re.sub(r"^```html?\s*", "", content) content = re.sub(r"\s*```$", "", content) log.info(f"KI-Analyse OK: {len(content)} Zeichen") return content else: log.error(f"OpenRouter Fehler {r.status_code}: {r.text[:200]}") return None except Exception as e: log.error(f"OpenRouter Exception: {e}") return None # ── Titel extrahieren ────────────────────────────────────────── def extract_title(content): m = re.search(r"