diff --git a/article_server.py.bak b/article_server.py.bak deleted file mode 100644 index 27621a5..0000000 --- a/article_server.py.bak +++ /dev/null @@ -1,1146 +0,0 @@ -#!/usr/bin/env python3 -"""Article Index Server — with IDs, meta extraction, image upload, and multi-threading. -v2: Added 'Markierte veröffentlichen' (Joomla API publish), article-image association, - and automatic VG-Wort tracking pixel injection.""" - -import os -import re -import glob -import json -import base64 -import hashlib -import socket -import urllib.parse -import io -import email.parser -import tempfile -from http.server import HTTPServer, BaseHTTPRequestHandler -from datetime import datetime -from socketserver import ThreadingMixIn - -WORKSPACE = "/home/hermes/workspace" -MEDIA_DIR = os.path.join(WORKSPACE, "media") -os.makedirs(MEDIA_DIR, exist_ok=True) -PORT = int(os.environ.get("ARTICLE_SERVER_PORT", "8890")) -ACCESS_TOKEN = "br3ttsp1el-n3ws-2026" -USERNAME = "DK-Adminchef" -PASSWORD = "Mcik7%vbdhXa_" - -# ── Joomla API Configuration ────────────────────────────────────────────── -JOOMLA_API_URL = "https://www.brettspiel-news.de/api/index.php/v1/content/articles" -# JOOMLA_TOKEN is read from environment or from a token file -JOOMLA_TOKEN = os.environ.get("JOOMLA_API_TOKEN", "") -if not JOOMLA_TOKEN: - token_paths = [ - "/tmp/alicia_skills/KEYS.txt", - "/home/hermes/.hermes/joomla_token.txt", - ] - for tp in token_paths: - if os.path.exists(tp): - with open(tp, "r") as f: - for line in f: - line = line.strip() - if line and not line.startswith("#"): - JOOMLA_TOKEN = line - break - if JOOMLA_TOKEN: - break - -# ── VG-Wort Configuration ───────────────────────────────────────────────── -VG_WORT_COUNTER_ID = os.environ.get("VG_WORT_COUNTER_ID", "") -VG_WORT_PIXEL_HTML = ( - f'' -) if VG_WORT_COUNTER_ID else "" - -# ── Article-Image Mapping ───────────────────────────────────────────────── -IMAGE_MAP_FILE = os.path.join(WORKSPACE, "article_images.json") - -MONTHS = { - "Januar": 1, "Februar": 2, "März": 3, "April": 4, - "Mai": 5, "Juni": 6, "Juli": 7, "August": 8, - "September": 9, "Oktober": 10, "November": 11, "Dezember": 12 -} - -SKIP_PATTERNS = [ - r'^ddg\d*\.html', r'^google', r'^bgg_(search|thread|game|price)', - r'^bk_', r'^backerkit', r'^brave_search', r'^fb_post', r'^reddit', - r'^rp_mg', r'^rp_gladbach', r'^spiegel', r'^sportschau', r'^transfermarkt', - r'^tm_', r'^kicker', r'^brettspielbox', r'^bsg_', r'^bbb_', - r'^envyborn', r'^brettspiel_news_newsletter', r'^boardgamewire', - r'^ddg_', r'^blocked', r'^error', r'^just.a.moment', - r'^duckduckgo', r'^update.regarding', -] - - -# ── Image Mapping Helpers ───────────────────────────────────────────────── -def get_image_map() -> dict[str, list[str]]: - """Load article→image mapping from JSON.""" - if os.path.exists(IMAGE_MAP_FILE): - with open(IMAGE_MAP_FILE, "r") as f: - return json.load(f) - return {} - - -def save_image_map(mapping: dict[str, list[str]]) -> None: - """Save article→image mapping to JSON.""" - with open(IMAGE_MAP_FILE, "w") as f: - json.dump(mapping, f, indent=2) - - -def is_article(filename: str) -> bool: - for pattern in SKIP_PATTERNS: - if re.search(pattern, filename, re.IGNORECASE): - return False - return filename.endswith(".html") - - -def extract_meta(filepath: str) -> dict | None: - """Extract title, date, meta description, keywords, and Joomla ID from an article.""" - with open(filepath, "r", encoding="utf-8") as f: - content = f.read() - - title_match = re.search(r'([^<]+)', content) - if not title_match: - return None - title = title_match.group(1) - - date_match = re.search(r'

(\d{1,2}\.\s*\w+\s*\d{4})', content) - if not date_match: - return None - date_str = date_match.group(1) - - try: - parts = date_str.replace(".", "").split() - day, month_name, year = int(parts[0]), parts[1], int(parts[2]) - month = MONTHS.get(month_name, 1) - sort_key = f"{year:04d}-{month:02d}-{day:02d}" - display_date = f"{day}.{month_name} {year}" - except (ValueError, IndexError): - return None - - meta_desc = "" - desc_match = re.search(r' list[dict]: - articles = [] - for f in glob.glob(os.path.join(WORKSPACE, "*.html")): - filename = os.path.basename(f) - if filename == "index.html": - continue - if not is_article(filename): - continue - meta = extract_meta(f) - if meta: - articles.append(meta) - - articles.sort(key=lambda a: (a["date_sort"], a["filename"]), reverse=True) - for i, a in enumerate(articles, 1): - a["local_id"] = i - return articles - - -def build_index(articles: list[dict]) -> str: - by_date_sort: dict[str, list[dict]] = {} - for a in articles: - by_date_sort.setdefault(a["date_sort"], []).append(a) - - month_names = {"01":"Januar","02":"Februar","03":"März","04":"April","05":"Mai","06":"Juni", - "07":"Juli","08":"August","09":"September","10":"Oktober","11":"November","12":"Dezember"} - - # Read image map for badge display - img_map = get_image_map() - - items_html = "" - for sort_key in sorted(by_date_sort.keys(), reverse=True): - day_articles = by_date_sort[sort_key] - try: - y, m, d = sort_key.split("-") - nice_date = f"{int(d)}. {month_names.get(m, m)} {y}" - except ValueError: - nice_date = day_articles[0]["date_display"] - items_html += f'

\n' - items_html += f'
{nice_date}{len(day_articles)}
\n' - for a in day_articles: - jid = a.get("joomla_id", "") - local_id = a.get("local_id", "") - id_str = f"#{jid}" if jid else f"#{local_id}" - id_class = "id-joomla" if jid else "id-local" - meta_desc = a.get("meta_desc", "") - meta_tags = a.get("meta_tags", "") - meta_preview = "" - if meta_desc or meta_tags: - meta_parts = [] - if meta_desc: - meta_parts.append(meta_desc[:120] + ("…" if len(meta_desc) > 120 else "")) - if meta_tags: - meta_parts.append(f'🏷 {meta_tags[:60]}{"…" if len(meta_tags) > 60 else ""}') - meta_preview = '
' + " · ".join(meta_parts) + '
' - - # Image badge - image_badge = "" - image_ok = False - if a["filename"] in img_map and img_map[a["filename"]]: - img_count = len(img_map[a["filename"]]) - image_badge = f'🖼{img_count}' - image_ok = True - - # Readiness indicators - meta_desc_ok = bool(meta_desc) - meta_tags_ok = bool(meta_tags) - all_ready = meta_desc_ok and meta_tags_ok and image_ok - - def ready_dot(ok: bool, title: str) -> str: - cls = "ready-ok" if ok else "ready-missing" - symbol = "●" if ok else "○" - return f'{symbol}' - - readiness = ( - ready_dot(meta_desc_ok, "Meta-Description") + - ready_dot(meta_tags_ok, "Keywords") + - ready_dot(image_ok, "Bild") - ) - - items_html += f"""
- - {id_str} - - {readiness} - {image_badge} -
\n""" - items_html += "
\n" - - count = len(articles) - has_token = bool(JOOMLA_TOKEN) - token_status = "🟢 API-Token vorhanden" if has_token else "🔴 Kein API-Token — Veröffentlichung deaktiviert" - publish_disabled = "" if has_token else "disabled" - publish_title = "" if has_token else ' title="Kein Joomla API-Token konfiguriert (JOOMLA_API_TOKEN env oder /tmp/alicia_skills/KEYS.txt)"' - - return f""" - - - - -Artikel-Übersicht · brettspiel-news.de - - - -
-
-

Artikel-Übersicht

-

{count} Artikel im Archiv · Aktualisieren{token_status}

-
-
-
-
- 📂 Rubriken (Kategorien) — zum Nachschlagen · 📌 Öffentliche = 3 Tage Hauptartikel -
-
61 Korrektur 🔒 Redaktion
-
9 Nachrichten
-
└ 64 Angebot
-
└ 65 Crowdfunding
-
└ 66 Branche
-
└ 67 Neuerscheinungen
-
└ 68 Ankündigungen
-
└ 69 Zubehör
-
8 Brettspieltest
-
11 Magazin
-
10 Video
-
55 Podcast
-
-
-
- - - - - - - 0 markiert -
-
-

Bild für Artikel hochladen

-

1. Wähle den Artikel-Dateinamen aus der Liste (z.B. artikel_spirit_island_retro_2017_2026-06-13.html)

-

2. Wähle die Bilddatei (.jpg, .png, .webp)

-

Das Bild wird dem Artikel zugeordnet und nach Veröffentlichung automatisch gelöscht.

-
- - -
-
-
- {items_html} -
-
- - -""" - - -class ThreadingArticleServer(ThreadingMixIn, HTTPServer): - """HTTPServer with threading support — handles multiple requests concurrently.""" - daemon_threads = True - allow_reuse_address = True - -class ArticleHandler(BaseHTTPRequestHandler): - timeout = 30 - - def _check_auth(self) -> bool: - auth_header = self.headers.get("Authorization", "") - if not auth_header.startswith("Basic "): - self._send_auth_required() - return False - try: - creds = base64.b64decode(auth_header[6:]).decode("utf-8") - username, password = creds.split(":", 1) - except Exception: - self._send_auth_required() - return False - if username == USERNAME and password == PASSWORD: - return True - self._send_auth_required() - return False - - def _send_auth_required(self): - self.send_response(401) - self.send_header("WWW-Authenticate", 'Basic realm="Artikel-Archiv"') - self.send_header("Content-Type", "text/html; charset=utf-8") - self.end_headers() - html = """ - -Zugriff verweigert - -

Zugriff verweigert

Bitte einloggen, um die Artikel-Übersicht zu sehen.

""" - self.wfile.write(html.encode("utf-8")) - - def _check_token(self) -> bool: - parsed = urllib.parse.urlparse(self.path) - params = urllib.parse.parse_qs(parsed.query) - key = params.get("key", [None])[0] - if key == ACCESS_TOKEN: - return True - self.send_response(403) - self.send_header("Content-Type", "text/html; charset=utf-8") - self.end_headers() - html = "Zugriff verweigert

Zugriff verweigert

Diese Seite ist nicht öffentlich.

" - self.wfile.write(html.encode("utf-8")) - return False - - def do_GET(self): - try: - self._do_GET_impl() - except (ConnectionResetError, BrokenPipeError, socket.timeout): - pass - except Exception as e: - try: - self.send_response(500) - self.end_headers() - self.wfile.write(b"Internal Server Error") - except Exception: - pass - print(f"[ERROR] {self.client_address} - {e}", flush=True) - - def _do_GET_impl(self): - if not self._check_token(): - return - - parsed = urllib.parse.urlparse(self.path) - path = parsed.path - - if path == "/" or path == "/index.html": - articles = get_articles() - html = build_index(articles) - self.send_response(200) - self.send_header("Content-Type", "text/html; charset=utf-8") - self.end_headers() - self.wfile.write(html.encode("utf-8")) - elif path.startswith("/media/"): - rel = path[len("/"):] - filepath = os.path.normpath(os.path.join(WORKSPACE, rel)) - if os.path.isfile(filepath) and filepath.startswith(WORKSPACE): - with open(filepath, "rb") as f: - data = f.read() - self.send_response(200) - self.send_header("Content-Length", str(len(data))) - ext = os.path.splitext(filepath)[1].lower() - mime_map = {".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", - ".webp": "image/webp", ".gif": "image/gif", ".svg": "image/svg+xml"} - self.send_header("Content-Type", mime_map.get(ext, "application/octet-stream")) - self.send_header("Cache-Control", "public, max-age=86400") - self.end_headers() - self.wfile.write(data) - else: - self.send_response(404) - self.end_headers() - elif path.startswith("/"): - filepath = os.path.join(WORKSPACE, path.lstrip("/")) - if os.path.isfile(filepath) and filepath.startswith(WORKSPACE): - with open(filepath, "rb") as f: - data = f.read() - self.send_response(200) - self.send_header("Content-Length", str(len(data))) - if filepath.endswith(".html"): - self.send_header("Content-Type", "text/html; charset=utf-8") - elif filepath.endswith(".css"): - self.send_header("Content-Type", "text/css") - elif filepath.endswith(".js"): - self.send_header("Content-Type", "application/javascript") - else: - self.send_header("Content-Type", "application/octet-stream") - self.end_headers() - self.wfile.write(data) - else: - self.send_response(404) - self.end_headers() - self.wfile.write(b"Not found") - else: - self.send_response(404) - self.end_headers() - - def do_POST(self): - try: - self._do_POST_impl() - except Exception as e: - print(f"[POST ERROR] {e}", flush=True) - try: - self._json_reply(500, {"ok": False, "error": str(e)}) - except Exception: - pass - - def _do_POST_impl(self): - parsed = urllib.parse.urlparse(self.path) - path = parsed.path - - if path == "/upload": - if not self._check_token(): - return - self._handle_upload() - elif path == "/delete": - if not self._check_auth(): - return - self._handle_delete() - elif path == "/publish": - if not self._check_auth(): - return - self._handle_publish() - else: - self.send_response(404) - self.end_headers() - - def _handle_upload(self): - """Handle multipart file upload — now accepts 'article' field for association.""" - content_type = self.headers.get("Content-Type", "") - if "multipart/form-data" not in content_type: - self._json_reply(400, {"ok": False, "error": "Expected multipart/form-data"}) - return - - content_length = int(self.headers.get("Content-Length", 0)) - body = self.rfile.read(content_length) - - boundary = None - for part in content_type.split(";"): - part = part.strip() - if part.startswith("boundary="): - boundary = part[9:].strip('"').strip("'").strip() - break - - if not boundary: - self._json_reply(400, {"ok": False, "error": "Kein Boundary in Content-Type"}) - return - - b_boundary = boundary.encode() - parts = body.split(b"--" + b_boundary) - file_data = None - filename = None - article_file = None - - for part in parts: - if b"Content-Disposition" not in part: - continue - header_end = part.find(b"\r\n\r\n") - if header_end == -1: - continue - headers = part[:header_end].decode("utf-8", errors="replace") - - if b"filename=" in part: - fn_match = re.search(r'filename="([^"]*)"', headers) - if fn_match: - filename = os.path.basename(fn_match.group(1)) - file_data = part[header_end + 4:] - if file_data: - file_data = file_data.rstrip(b"\r\n").rstrip(b"-") - elif 'name="article"' in headers: - article_file = part[header_end + 4:].decode("utf-8", errors="replace").strip() - if article_file: - article_file = article_file.rstrip("\r\n").rstrip("-") - - if not file_data or not filename: - self._json_reply(400, {"ok": False, "error": "Keine Datei gefunden"}) - return - - ext = os.path.splitext(filename)[1].lower() - if ext not in (".jpg", ".jpeg", ".png", ".webp", ".gif", ".svg"): - self._json_reply(400, {"ok": False, "error": f"Nicht erlaubtes Format: {ext}. Erlaubt: jpg, png, webp, gif, svg"}) - return - - now = datetime.now() - month_dir = os.path.join(MEDIA_DIR, now.strftime("%Y"), now.strftime("%m")) - os.makedirs(month_dir, exist_ok=True) - - base, ext = os.path.splitext(filename) - safe_base = re.sub(r'[^a-zA-Z0-9_-]', '_', base) - unique_name = f"{safe_base}_{now.strftime('%H%M%S')}{ext}" - filepath = os.path.join(month_dir, unique_name) - - with open(filepath, "wb") as f: - f.write(data) - - # ── Article-Image Association ────────────────────────────────────── - association_msg = "" - if article_file: - img_map = get_image_map() - if article_file not in img_map: - img_map[article_file] = [] - img_map[article_file].append(filepath) - save_image_map(img_map) - association_msg = f" → zugeordnet zu: {article_file}" - - rel_path = os.path.relpath(filepath, WORKSPACE) - self._json_reply(200, { - "ok": True, - "filename": unique_name, - "path": filepath, - "url": f"MEDIA:{filepath}", - "html_src": f"/media/{now.strftime('%Y')}/{now.strftime('%m')}/{unique_name}", - "article": article_file, - }) - - def _handle_delete(self): - length = int(self.headers.get("Content-Length", 0)) - body = self.rfile.read(length) - try: - data = json.loads(body) - files = data.get("files", []) - except json.JSONDecodeError: - self._json_reply(400, {"ok": False, "error": "Invalid JSON"}) - return - - deleted = 0 - errors = [] - for fname in files: - fname = os.path.basename(fname) - if not fname.endswith(".html"): - errors.append(f"{fname}: not an HTML file") - continue - filepath = os.path.join(WORKSPACE, fname) - if not os.path.exists(filepath): - errors.append(f"{fname}: not found") - continue - try: - os.remove(filepath) - deleted += 1 - except OSError as e: - errors.append(f"{fname}: {e}") - - self._json_reply(200, {"ok": True, "deleted": deleted, "errors": errors}) - - def _handle_publish(self): - """Publish selected articles via Joomla API with VG-Wort pixel injection - and automatic cleanup of associated images. - - SAFETY GATE (Daniel 18.06.): Ohne 'live': true im Request-Payload - gehen ALLE Artikel auf Korrektur (catid=61, state=0) — niemals live. - """ - if not JOOMLA_TOKEN: - self._json_reply(400, { - "ok": False, - "error": "Kein Joomla API-Token konfiguriert. " - "Bitte JOOMLA_API_TOKEN Umgebungsvariable setzen " - "oder /tmp/alicia_skills/KEYS.txt anlegen." - }) - return - - length = int(self.headers.get("Content-Length", 0)) - body = self.rfile.read(length) - try: - data = json.loads(body) - files = data.get("files", []) - allow_live = data.get("live", False) # SAFETY GATE: Nur wenn explizit true - except json.JSONDecodeError: - self._json_reply(400, {"ok": False, "error": "Invalid JSON"}) - return - - published = 0 - images_deleted_total = 0 - errors = [] - img_map = get_image_map() - - # Safety log - mode = "🔴 LIVE (state=1)" if allow_live else "🟡 KORREKTUR (state=0, Redaktion-only)" - print(f"[PUBLISH] {len(files)} Artikel, Modus: {mode}", flush=True) - - # Try importing requests - try: - import requests - except ImportError: - self._json_reply(500, { - "ok": False, - "error": "Python 'requests' Modul nicht installiert. " - "Bitte ausführen: /home/hermes/venv/bin/pip install requests" - }) - return - - for fname in files: - fname = os.path.basename(fname) - if not fname.endswith(".html"): - errors.append(f"{fname}: not an HTML file") - continue - filepath = os.path.join(WORKSPACE, fname) - if not os.path.exists(filepath): - errors.append(f"{fname}: not found") - continue - - try: - with open(filepath, "r", encoding="utf-8") as f: - html_content = f.read() - - # ── Extract article metadata ─────────────────────────────── - title_match = re.search(r'([^<]+)', html_content) - if not title_match: - errors.append(f"{fname}: kein gefunden") - continue - title = title_match.group(1) - - # ── Readiness-Warnung (soft, blockiert nicht) ─────────── - readiness_warnings = [] - - meta_desc_match = re.search( - r'<meta\s+name="description"\s+content="([^"]+)"', - html_content, re.IGNORECASE - ) - if not (meta_desc_match and meta_desc_match.group(1).strip()): - readiness_warnings.append("keine Meta-Description") - - meta_key_match = re.search( - r'<meta\s+name="keywords"\s+content="([^"]+)"', - html_content, re.IGNORECASE - ) - if not (meta_key_match and meta_key_match.group(1).strip()): - readiness_warnings.append("keine Meta-Keywords") - - if fname not in img_map or not img_map[fname]: - readiness_warnings.append("kein Bild zugewiesen") - - if readiness_warnings: - # Soft warning — Publizieren trotzdem möglich - errors.append(f"{fname}: ⚠️ {', '.join(readiness_warnings)} (trotzdem publiziert)") - - # Extract the article body (content inside <article> or <body>) - body_match = re.search(r'<article[^>]*>(.*?)</article>', html_content, re.DOTALL) - if not body_match: - # Fallback: use everything between </header> and <footer> or end - body_match = re.search(r'(?:</header>|</head>)(.*?)(?:<footer|<div class="quellen"|</body)', html_content, re.DOTALL) - article_body = body_match.group(1) if body_match else html_content - - # Extract Joomla ID if present (for updates) - jid_match = re.search(r'<meta\s+name="joomla-id"\s+content="(\d+)"', html_content, re.IGNORECASE) - joomla_id = jid_match.group(1) if jid_match else None - - # ── SAFETY GATE: Ohne live=true ALLES auf Korrektur ───── - # Extract category from meta – NUR wenn live explizit erlaubt - catid = 61 # Default: Korrektur (Redaktion-only, nicht öffentlich) - if allow_live: - cat_match = re.search(r'<meta\s+name="category-id"\s+content="(\d+)"', html_content, re.IGNORECASE) - if cat_match: - catid = int(cat_match.group(1)) - - # Korrektur (61) → state=0 (unpublished/Redaktion-only) - # Alles andere → state=1 (published/public) – NUR bei allow_live - state = 0 if catid == 61 else 1 - access = 6 if catid == 61 else 1 # 6=Redaktion, 1=Public - - # ── Featured (Hauptartikel) ───────────────────────── - featured = 0 - featured_up = None - featured_down = None - if allow_live and catid != 61: - # Default: 3 Tage featured, via <meta name="featured-days"> anpassbar - featured_days = 3 - fd_match = re.search( - r'<meta\s+name="featured-days"\s+content="(\d+)"', - html_content, re.IGNORECASE - ) - if fd_match: - featured_days = int(fd_match.group(1)) - - from datetime import datetime as dt, timedelta - now = dt.now() - featured = 1 - featured_up = now.strftime("%Y-%m-%d %H:%M:%S") - featured_down = (now + timedelta(days=featured_days)).strftime("%Y-%m-%d %H:%M:%S") - - # Generate alias from title - alias = re.sub(r'[^a-z0-9]+', '-', title.lower().strip())[:100] - - # ── VG-Wort Zählmarke aus Pool holen & einbauen ───────── - # NUR bei LIVE-Artikeln (state=1)! Korrektur verbraucht keine Zählmarke. - vgwort_pixel_html = "" - if state == 1: - try: - import subprocess - result = subprocess.run( - ["python3", os.path.expanduser("~/.hermes/vgwort_pool.py"), "get"], - capture_output=True, text=True, timeout=30 - ) - if result.returncode == 0: - pixel_data = json.loads(result.stdout) - if pixel_data.get("ok"): - vgwort_pixel_html = pixel_data.get("pixel_html", "") - print(f"[VG-Wort] Zählmarke {pixel_data.get('public_id')} eingebaut, " - f"Pool: {pixel_data.get('pool_remaining')} übrig", flush=True) - else: - print(f"[VG-Wort] FEHLER: {pixel_data.get('error', 'unbekannt')}", flush=True) - else: - print(f"[VG-Wort] Pool-Script fehlgeschlagen: {result.stderr}", flush=True) - except Exception as e: - print(f"[VG-Wort] Ausnahme: {e}", flush=True) - else: - print(f"[VG-Wort] Übersprungen — Artikel geht auf Korrektur (state=0), " - f"Zählmarke erst bei Live-Gang", flush=True) - - if vgwort_pixel_html: - if '</article>' in article_body: - article_body = article_body.replace('</article>', f'{vgwort_pixel_html}\n</article>') - else: - article_body = article_body.rstrip() + f'\n{vgwort_pixel_html}\n' - - # ── Joomla API call ───────────────────────────────────────── - headers = { - "Accept": "application/vnd.api+json", - "Content-Type": "application/json", - "X-Joomla-Token": JOOMLA_TOKEN, - } - - article_payload = { - "title": title, - "alias": alias, - "articletext": article_body.strip(), - "catid": catid, - "language": "*", - "state": state, - "access": access, - "featured": featured, - } - if featured_up: - article_payload["featured_up"] = featured_up - if featured_down: - article_payload["featured_down"] = featured_down - - if joomla_id: - # Update existing article - resp = requests.patch( - f"{JOOMLA_API_URL}/{joomla_id}", - headers=headers, json=article_payload, timeout=30 - ) - else: - # Create new article - resp = requests.post( - JOOMLA_API_URL, - headers=headers, json=article_payload, timeout=30 - ) - - if resp.status_code in (200, 201): - published += 1 - - # ── Write back Joomla ID to HTML ──────────────────────── - resp_data = resp.json() - new_jid = None - if "data" in resp_data and "id" in resp_data["data"]: - new_jid = str(resp_data["data"]["id"]) - elif "data" in resp_data and "attributes" in resp_data["data"]: - # Some responses nest the id differently - pass - - if new_jid and not joomla_id: - # Inject <meta name="joomla-id"> into the HTML file - if '<meta name="joomla-id"' not in html_content: - jid_meta = f'\n<meta name="joomla-id" content="{new_jid}">' - html_content = re.sub( - r'(<meta[^>]*>)', - lambda m: m.group(1) + jid_meta, - html_content, - count=1 - ) - else: - html_content = re.sub( - r'<meta\s+name="joomla-id"\s+content="(\d+)"', - f'<meta name="joomla-id" content="{new_jid}"', - html_content - ) - with open(filepath, "w", encoding="utf-8") as f: - f.write(html_content) - - # ── Auto-delete associated images ─────────────────────── - if fname in img_map: - for img_path in img_map[fname]: - if os.path.exists(img_path): - try: - os.remove(img_path) - images_deleted_total += 1 - except OSError as e: - errors.append(f"{fname}: Bild {img_path} konnte nicht gelöscht werden: {e}") - del img_map[fname] - save_image_map(img_map) - - else: - error_msg = f"HTTP {resp.status_code}" - try: - err_data = resp.json() - if "errors" in err_data: - error_msg += ": " + str(err_data["errors"][0].get("title", err_data["errors"])) - except Exception: - error_msg += ": " + resp.text[:200] - errors.append(f"{fname}: {error_msg}") - - except Exception as e: - errors.append(f"{fname}: {e}") - - self._json_reply(200 if published > 0 else 500, { - "ok": published > 0, - "published": published, - "live_mode": allow_live, - "images_deleted": images_deleted_total, - "errors": errors, - "vg_wort_enabled": not allow_live or True, # immer enabled, aber nur bei live genutzt - }) - - def _json_reply(self, status, data): - self.send_response(status) - self.send_header("Content-Type", "application/json") - self.end_headers() - self.wfile.write(json.dumps(data).encode("utf-8")) - - -def main(): - import sys - os.chdir(WORKSPACE) - server = ThreadingArticleServer(("0.0.0.0", PORT), ArticleHandler) - has_token = "JA" if JOOMLA_TOKEN else "NEIN (Veröffentlichung deaktiviert)" - has_vgwort = VG_WORT_COUNTER_ID or "NICHT KONFIGURIERT" - print(f"Article server running on http://0.0.0.0:{PORT}") - print(f" Joomla-Token: {has_token}") - print(f" VG-Wort-ID: {has_vgwort}") - sys.stdout.flush() - try: - server.serve_forever() - except KeyboardInterrupt: - print("\nShutting down.") - server.server_close() - - -if __name__ == "__main__": - main()