From 5f923d8eced983a009d02b6b40f2160eba1d896e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Lohrer?= Date: Wed, 1 Oct 2025 06:16:10 +0200 Subject: [PATCH] Initial commit: WordPress News Import System --- .env.example | 4 + .gitignore | 28 ++++ README.md | 281 ++++++++++++++++++++++++++++++++++++ content/beispiel-beitrag.md | 25 ++++ posts.yaml | 50 +++++++ requirements.txt | 4 + wordpress_api.py | 278 +++++++++++++++++++++++++++++++++++ workflow.py | 278 +++++++++++++++++++++++++++++++++++ 8 files changed, 948 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 content/beispiel-beitrag.md create mode 100644 posts.yaml create mode 100644 requirements.txt create mode 100644 wordpress_api.py create mode 100644 workflow.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..810cbcf --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +# WordPress API Credentials +WORDPRESS_URL=https://news.rpi-virtuell.de +WORDPRESS_USERNAME=your_username +WORDPRESS_APP_PASSWORD=UIVI 4Tdy oojL 9iZG g3X2 iAn5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dbd47b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Environment variables +.env +.env.local + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +env/ +ENV/ +.venv + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Logs +*.log + +# OS +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..cb9bf07 --- /dev/null +++ b/README.md @@ -0,0 +1,281 @@ +# WordPress News Import + +Automatisierter Workflow zum Erstellen von WordPress-Beiträgen aus Markdown-Dateien über die WordPress REST-API. + +## Features + +- ✅ **Duplikatsprüfung**: Verhindert das doppelte Erstellen von Beiträgen und Medien +- ✅ **Markdown zu HTML**: Automatische Konvertierung von Markdown-Inhalten +- ✅ **Medien-Upload**: Hochladen von Beitragsbildern mit Duplikatsprüfung +- ✅ **Kategorien & Tags**: Automatische Erstellung fehlender Kategorien und Tags +- ✅ **Flexible Quellen**: Unterstützt Markdown-URLs und lokale Dateien +- ✅ **YAML-Konfiguration**: Einfache Verwaltung mehrerer Beiträge + +## Voraussetzungen + +- Python 3.7 oder höher +- WordPress-Installation mit aktivierter REST-API +- WordPress Anwendungspasswort (Application Password) + +## Installation + +1. **Repository klonen oder herunterladen** + +2. **Python-Abhängigkeiten installieren** + ```bash + pip install -r requirements.txt + ``` + +3. **Umgebungsvariablen konfigurieren** + + Kopieren Sie `.env.example` zu `.env` und tragen Sie Ihre Credentials ein: + ```bash + cp .env.example .env + ``` + + Bearbeiten Sie `.env`: + ```env + WORDPRESS_URL=https://news.rpi-virtuell.de + WORDPRESS_USERNAME=ihr_benutzername + WORDPRESS_APP_PASSWORD=UIVI 4Tdy oojL 9iZG g3X2 iAn5 + ``` + +## WordPress Anwendungspasswort erstellen + +1. Melden Sie sich in WordPress an +2. Gehen Sie zu **Benutzer → Profil** +3. Scrollen Sie zu **Anwendungspasswörter** +4. Geben Sie einen Namen ein (z.B. "News Import") +5. Klicken Sie auf **Neues Anwendungspasswort hinzufügen** +6. Kopieren Sie das generierte Passwort in Ihre `.env`-Datei + +## Verwendung + +### 1. YAML-Konfiguration erstellen + +Erstellen Sie eine `posts.yaml`-Datei mit Ihren Beiträgen: + +```yaml +posts: + - title: "Mein erster Beitrag" + markdown_url: "https://raw.githubusercontent.com/user/repo/main/post.md" + status: "draft" # draft, publish, pending, private + categories: + - "News" + - "Tutorials" + tags: + - "WordPress" + - "API" + featured_image: "images/header.jpg" + excerpt: "Eine kurze Zusammenfassung" + + - title: "Lokaler Beitrag" + markdown_file: "content/local-post.md" + status: "publish" + categories: + - "Updates" + +settings: + default_status: "draft" + skip_duplicates: true + skip_duplicate_media: true +``` + +### 2. Workflow ausführen + +```bash +python workflow.py posts.yaml +``` + +Oder ohne Angabe der Datei (verwendet `posts.yaml` als Standard): + +```bash +python workflow.py +``` + +## Struktur + +``` +newsimport/ +├── .env # Credentials (nicht in Git!) +├── .env.example # Beispiel-Konfiguration +├── .gitignore # Git-Ignorier-Liste +├── requirements.txt # Python-Abhängigkeiten +├── wordpress_api.py # WordPress REST-API Client +├── workflow.py # Haupt-Workflow Script +├── posts.yaml # Beitrags-Konfiguration +├── README.md # Diese Datei +├── content/ # Lokale Markdown-Dateien (optional) +│ └── *.md +└── images/ # Lokale Bilder (optional) + └── *.jpg/png +``` + +## API-Funktionen + +### WordPress API Client (`wordpress_api.py`) + +```python +from wordpress_api import WordPressAPI + +# API initialisieren +wp = WordPressAPI(url, username, app_password) + +# Beitrag erstellen (mit Duplikatsprüfung) +post_id = wp.create_post( + title="Titel", + content="

HTML-Inhalt

", + status="publish", + check_duplicate=True +) + +# Medien hochladen (mit Duplikatsprüfung) +media_id = wp.upload_media( + file_path="bild.jpg", + title="Bild-Titel", + alt_text="Alt-Text", + check_duplicate=True +) + +# Kategorie holen oder erstellen +cat_id = wp.get_or_create_category("News") + +# Tag holen oder erstellen +tag_id = wp.get_or_create_tag("WordPress") + +# Auf Duplikate prüfen +existing_post_id = wp.check_post_exists("Titel") +existing_media_id = wp.check_media_exists("bild.jpg") +``` + +## YAML-Konfiguration + +### Beitrags-Felder + +- `title` (erforderlich): Titel des Beitrags +- `markdown_url`: URL zur Markdown-Datei +- `markdown_file`: Pfad zu lokaler Markdown-Datei +- `content`: Direkter Markdown-Inhalt +- `status`: `draft`, `publish`, `pending`, `private` +- `categories`: Liste von Kategorie-Namen +- `tags`: Liste von Tag-Namen +- `featured_image`: Pfad oder URL zum Beitragsbild +- `excerpt`: Kurze Zusammenfassung +- `author`: Autor-Username + +### Globale Einstellungen + +```yaml +settings: + default_status: "draft" # Standard-Status für Beiträge + default_author: "admin" # Standard-Autor + skip_duplicates: true # Bestehende Beiträge überspringen + skip_duplicate_media: true # Bestehende Medien überspringen + markdown_extensions: # Markdown-Erweiterungen + - tables + - fenced_code + - footnotes +``` + +## Duplikatsprüfung + +### Beiträge +Das System prüft vor dem Erstellen, ob ein Beitrag mit dem gleichen Titel bereits existiert. Falls ja, wird die bestehende Post-ID zurückgegeben und kein neuer Beitrag erstellt. + +### Medien +Vor dem Upload wird geprüft, ob eine Datei mit dem gleichen Namen bereits existiert. Falls ja, wird die bestehende Media-ID verwendet. + +## Beispiele + +### Beispiel 1: Einfacher Beitrag von URL + +```yaml +posts: + - title: "News Update" + markdown_url: "https://example.com/news.md" + status: "publish" +``` + +### Beispiel 2: Beitrag mit Kategorien, Tags und Bild + +```yaml +posts: + - title: "Tutorial: WordPress REST-API" + markdown_url: "https://example.com/tutorial.md" + status: "draft" + categories: + - "Tutorials" + - "WordPress" + tags: + - "REST-API" + - "Entwicklung" + - "PHP" + featured_image: "https://example.com/images/header.jpg" + excerpt: "Lernen Sie die WordPress REST-API kennen" +``` + +### Beispiel 3: Lokale Dateien + +```yaml +posts: + - title: "Lokaler Inhalt" + markdown_file: "content/article.md" + status: "publish" + featured_image: "images/local-image.jpg" +``` + +## Fehlerbehebung + +### Authentifizierungsfehler + +**Problem**: `401 Unauthorized` + +**Lösung**: +- Überprüfen Sie Username und Anwendungspasswort in `.env` +- Stellen Sie sicher, dass das Anwendungspasswort korrekt ist (keine zusätzlichen Leerzeichen) +- Verifizieren Sie, dass die WordPress REST-API aktiviert ist + +### Keine Verbindung zu WordPress + +**Problem**: `Connection refused` oder Timeout + +**Lösung**: +- Überprüfen Sie die `WORDPRESS_URL` in `.env` +- Stellen Sie sicher, dass WordPress erreichbar ist +- Prüfen Sie Firewall-Einstellungen + +### Markdown wird nicht konvertiert + +**Problem**: Markdown-Syntax erscheint im Beitrag + +**Lösung**: +- Überprüfen Sie, ob `markdown` installiert ist: `pip install markdown` +- Prüfen Sie die Markdown-Syntax in der Quelldatei + +### Import-Fehler bei Modulen + +**Problem**: `ModuleNotFoundError: No module named 'requests'` + +**Lösung**: +```bash +pip install -r requirements.txt +``` + +## Sicherheit + +⚠️ **Wichtig**: +- Committen Sie **niemals** die `.env`-Datei mit echten Credentials in Git! +- Die `.gitignore` ist bereits so konfiguriert, dass `.env` ignoriert wird +- Verwenden Sie `.env.example` als Vorlage für andere Nutzer + +## Lizenz + +Dieses Projekt steht unter der MIT-Lizenz. + +## Support + +Bei Problemen oder Fragen erstellen Sie bitte ein Issue im Repository. + +--- + +**Hinweis**: Dieses Tool wurde für `https://news.rpi-virtuell.de` entwickelt, funktioniert aber mit jeder WordPress-Installation, die die REST-API unterstützt. diff --git a/content/beispiel-beitrag.md b/content/beispiel-beitrag.md new file mode 100644 index 0000000..1d21466 --- /dev/null +++ b/content/beispiel-beitrag.md @@ -0,0 +1,25 @@ +# Beispiel-Beitrag + +Dies ist ein Beispiel für einen lokalen Markdown-Beitrag. + +## Einführung + +Dieser Beitrag demonstriert die Verwendung von lokalem Markdown-Content für WordPress-Beiträge. + +## Features + +- **Markdown-Formatierung**: Vollständige Markdown-Unterstützung +- **Code-Blöcke**: Syntax-Highlighting für verschiedene Sprachen +- **Listen**: Geordnet und ungeordnet +- **Links und Bilder**: Vollständige Unterstützung + +## Code-Beispiel + +```python +def hello_world(): + print("Hello, WordPress!") +``` + +## Zusammenfassung + +Mit diesem System können Sie einfach Markdown-Inhalte in WordPress-Beiträge umwandeln. diff --git a/posts.yaml b/posts.yaml new file mode 100644 index 0000000..cf66e7d --- /dev/null +++ b/posts.yaml @@ -0,0 +1,50 @@ +# Beispiel-Konfiguration für WordPress-Beiträge +# Diese YAML-Datei definiert Beiträge, die aus Markdown-Dateien erstellt werden sollen + +posts: + - title: "Einführung in die WordPress REST-API" + markdown_url: "https://raw.githubusercontent.com/example/repo/main/docs/wordpress-api.md" + status: "draft" # draft, publish, pending, private + categories: + - "Tutorials" + - "WordPress" + tags: + - "REST-API" + - "Entwicklung" + featured_image: "images/wordpress-api-header.jpg" + author: "admin" + excerpt: "Ein umfassender Leitfaden zur Verwendung der WordPress REST-API" + + - title: "Best Practices für WordPress-Plugins" + markdown_url: "https://raw.githubusercontent.com/example/repo/main/docs/plugin-best-practices.md" + status: "draft" + categories: + - "WordPress" + - "Best Practices" + tags: + - "Plugins" + - "Entwicklung" + - "Sicherheit" + + - title: "Lokale Markdown-Datei verwenden" + markdown_file: "content/local-post.md" # Lokale Datei statt URL + status: "publish" + categories: + - "News" + tags: + - "Update" + featured_image: "images/news-header.png" + +# Globale Einstellungen (optional) +settings: + default_status: "draft" + default_author: "admin" + # Wenn true, werden vorhandene Beiträge übersprungen + skip_duplicates: true + # Wenn true, werden vorhandene Medien übersprungen + skip_duplicate_media: true + # Markdown zu HTML Konvertierung + markdown_extensions: + - tables + - fenced_code + - footnotes diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..910ee01 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +requests>=2.31.0 +python-dotenv>=1.0.0 +PyYAML>=6.0.1 +markdown>=3.5.0 diff --git a/wordpress_api.py b/wordpress_api.py new file mode 100644 index 0000000..0368463 --- /dev/null +++ b/wordpress_api.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +""" +WordPress API Helper +Bietet Funktionen zum Erstellen von WordPress-Beiträgen und Hochladen von Medien +über die WordPress REST-API mit Duplikatsprüfung. +""" + +import os +import requests +import hashlib +from typing import Optional, Dict, Any, List +from urllib.parse import urljoin +import mimetypes + + +class WordPressAPI: + """WordPress REST-API Client mit Duplikatsprüfung""" + + def __init__(self, url: str, username: str, app_password: str): + """ + Initialisiert den WordPress API Client + + Args: + url: WordPress URL (z.B. https://news.rpi-virtuell.de) + username: WordPress Benutzername + app_password: WordPress Anwendungspasswort + """ + self.url = url.rstrip('/') + self.api_base = urljoin(self.url, '/wp-json/wp/v2/') + self.auth = (username, app_password.replace(' ', '')) + self.session = requests.Session() + self.session.auth = self.auth + + def _get(self, endpoint: str, params: Optional[Dict] = None) -> requests.Response: + """GET-Request an WordPress API""" + url = urljoin(self.api_base, endpoint) + response = self.session.get(url, params=params) + response.raise_for_status() + return response + + def _post(self, endpoint: str, data: Optional[Dict] = None, + files: Optional[Dict] = None, headers: Optional[Dict] = None) -> requests.Response: + """POST-Request an WordPress API""" + url = urljoin(self.api_base, endpoint) + response = self.session.post(url, json=data, files=files, headers=headers) + response.raise_for_status() + return response + + def check_post_exists(self, title: str) -> Optional[int]: + """ + Prüft, ob ein Beitrag mit dem Titel bereits existiert + + Args: + title: Titel des Beitrags + + Returns: + Post-ID wenn gefunden, sonst None + """ + try: + response = self._get('posts', params={'search': title, 'per_page': 10}) + posts = response.json() + + # Exakte Übereinstimmung prüfen + for post in posts: + if post.get('title', {}).get('rendered', '') == title: + return post['id'] + return None + except requests.exceptions.RequestException as e: + print(f"Fehler bei der Suche nach Beitrag: {e}") + return None + + def check_media_exists(self, filename: str, file_hash: Optional[str] = None) -> Optional[int]: + """ + Prüft, ob eine Mediendatei bereits existiert + + Args: + filename: Dateiname + file_hash: Optional MD5-Hash der Datei für präzisere Prüfung + + Returns: + Media-ID wenn gefunden, sonst None + """ + try: + # Suche nach Dateiname + response = self._get('media', params={'search': filename, 'per_page': 10}) + media_items = response.json() + + for item in media_items: + source_url = item.get('source_url', '') + if filename in source_url: + # Wenn Hash gegeben, zusätzlich prüfen + if file_hash: + # Hash kann nicht direkt verglichen werden, + # daher nur Dateiname-Check + return item['id'] + return item['id'] + return None + except requests.exceptions.RequestException as e: + print(f"Fehler bei der Suche nach Medien: {e}") + return None + + def upload_media(self, file_path: str, title: Optional[str] = None, + alt_text: Optional[str] = None, + check_duplicate: bool = True) -> Optional[int]: + """ + Lädt eine Mediendatei zu WordPress hoch + + Args: + file_path: Pfad zur Datei + title: Titel der Mediendatei (optional) + alt_text: Alt-Text für Bilder (optional) + check_duplicate: Prüfung auf Duplikate (Standard: True) + + Returns: + Media-ID der hochgeladenen Datei, oder None bei Fehler + """ + if not os.path.exists(file_path): + print(f"Datei nicht gefunden: {file_path}") + return None + + filename = os.path.basename(file_path) + + # Duplikatsprüfung + if check_duplicate: + existing_id = self.check_media_exists(filename) + if existing_id: + print(f"Medien-Datei '{filename}' existiert bereits (ID: {existing_id})") + return existing_id + + # MIME-Type ermitteln + mime_type, _ = mimetypes.guess_type(file_path) + if not mime_type: + mime_type = 'application/octet-stream' + + # Datei hochladen + try: + with open(file_path, 'rb') as f: + files = { + 'file': (filename, f, mime_type) + } + + headers = { + 'Content-Disposition': f'attachment; filename="{filename}"' + } + + if title: + headers['Content-Title'] = title + if alt_text: + headers['Content-Alt-Text'] = alt_text + + url = urljoin(self.api_base, 'media') + response = self.session.post(url, files=files, headers=headers) + response.raise_for_status() + + media_data = response.json() + media_id = media_data['id'] + print(f"Medien-Datei '{filename}' hochgeladen (ID: {media_id})") + return media_id + + except requests.exceptions.RequestException as e: + print(f"Fehler beim Hochladen der Medien-Datei: {e}") + return None + + def create_post(self, title: str, content: str, + status: str = 'draft', + featured_media: Optional[int] = None, + categories: Optional[List[int]] = None, + tags: Optional[List[int]] = None, + check_duplicate: bool = True, + **kwargs) -> Optional[int]: + """ + Erstellt einen neuen WordPress-Beitrag + + Args: + title: Titel des Beitrags + content: Inhalt des Beitrags (HTML) + status: Status (draft, publish, etc.) + featured_media: ID des Beitragsbilds + categories: Liste der Kategorie-IDs + tags: Liste der Tag-IDs + check_duplicate: Prüfung auf Duplikate (Standard: True) + **kwargs: Weitere WordPress-Post-Felder + + Returns: + Post-ID des erstellten Beitrags, oder None bei Fehler + """ + # Duplikatsprüfung + if check_duplicate: + existing_id = self.check_post_exists(title) + if existing_id: + print(f"Beitrag '{title}' existiert bereits (ID: {existing_id})") + return existing_id + + # Post-Daten zusammenstellen + post_data = { + 'title': title, + 'content': content, + 'status': status, + **kwargs + } + + if featured_media: + post_data['featured_media'] = featured_media + if categories: + post_data['categories'] = categories + if tags: + post_data['tags'] = tags + + # Beitrag erstellen + try: + response = self._post('posts', data=post_data) + post = response.json() + post_id = post['id'] + print(f"Beitrag '{title}' erstellt (ID: {post_id}, Status: {status})") + return post_id + + except requests.exceptions.RequestException as e: + print(f"Fehler beim Erstellen des Beitrags: {e}") + if hasattr(e.response, 'text'): + print(f"Details: {e.response.text}") + return None + + def get_categories(self) -> List[Dict[str, Any]]: + """Holt alle verfügbaren Kategorien""" + try: + response = self._get('categories', params={'per_page': 100}) + return response.json() + except requests.exceptions.RequestException as e: + print(f"Fehler beim Abrufen der Kategorien: {e}") + return [] + + def get_or_create_category(self, name: str) -> Optional[int]: + """Holt oder erstellt eine Kategorie""" + categories = self.get_categories() + for cat in categories: + if cat['name'].lower() == name.lower(): + return cat['id'] + + # Kategorie erstellen + try: + response = self._post('categories', data={'name': name}) + return response.json()['id'] + except requests.exceptions.RequestException as e: + print(f"Fehler beim Erstellen der Kategorie: {e}") + return None + + def get_tags(self) -> List[Dict[str, Any]]: + """Holt alle verfügbaren Tags""" + try: + response = self._get('tags', params={'per_page': 100}) + return response.json() + except requests.exceptions.RequestException as e: + print(f"Fehler beim Abrufen der Tags: {e}") + return [] + + def get_or_create_tag(self, name: str) -> Optional[int]: + """Holt oder erstellt einen Tag""" + tags = self.get_tags() + for tag in tags: + if tag['name'].lower() == name.lower(): + return tag['id'] + + # Tag erstellen + try: + response = self._post('tags', data={'name': name}) + return response.json()['id'] + except requests.exceptions.RequestException as e: + print(f"Fehler beim Erstellen des Tags: {e}") + return None + + +def calculate_file_hash(file_path: str) -> str: + """Berechnet MD5-Hash einer Datei""" + hash_md5 = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_md5.update(chunk) + return hash_md5.hexdigest() diff --git a/workflow.py b/workflow.py new file mode 100644 index 0000000..44efc95 --- /dev/null +++ b/workflow.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +""" +WordPress Import Workflow +Liest Markdown-Dateien aus URLs oder lokalen Dateien und erstellt WordPress-Beiträge +basierend auf einer YAML-Konfigurationsdatei. +""" + +import os +import sys +import yaml +import requests +import markdown +from pathlib import Path +from dotenv import load_dotenv +from typing import Dict, Any, List, Optional +from wordpress_api import WordPressAPI + +# Lade Umgebungsvariablen +load_dotenv() + + +def download_markdown(url: str) -> Optional[str]: + """ + Lädt Markdown-Inhalt von einer URL herunter + + Args: + url: URL zur Markdown-Datei + + Returns: + Markdown-Inhalt als String oder None bei Fehler + """ + try: + response = requests.get(url, timeout=30) + response.raise_for_status() + return response.text + except requests.exceptions.RequestException as e: + print(f"Fehler beim Herunterladen von {url}: {e}") + return None + + +def read_local_markdown(file_path: str) -> Optional[str]: + """ + Liest Markdown-Inhalt aus einer lokalen Datei + + Args: + file_path: Pfad zur lokalen Markdown-Datei + + Returns: + Markdown-Inhalt als String oder None bei Fehler + """ + try: + with open(file_path, 'r', encoding='utf-8') as f: + return f.read() + except IOError as e: + print(f"Fehler beim Lesen von {file_path}: {e}") + return None + + +def markdown_to_html(markdown_text: str, extensions: Optional[List[str]] = None) -> str: + """ + Konvertiert Markdown zu HTML + + Args: + markdown_text: Markdown-Text + extensions: Liste der Markdown-Erweiterungen + + Returns: + HTML-String + """ + if extensions is None: + extensions = ['extra', 'codehilite', 'toc'] + + return markdown.markdown(markdown_text, extensions=extensions) + + +def process_featured_image(wp_api: WordPressAPI, image_path: str, + check_duplicate: bool = True) -> Optional[int]: + """ + Verarbeitet und lädt ein Beitragsbild hoch + + Args: + wp_api: WordPress API Client + image_path: Pfad zum Bild (lokal oder URL) + check_duplicate: Prüfung auf Duplikate + + Returns: + Media-ID oder None + """ + # Prüfe ob URL oder lokaler Pfad + if image_path.startswith('http://') or image_path.startswith('https://'): + # Download Bild + try: + response = requests.get(image_path, timeout=30) + response.raise_for_status() + + # Temporäre Datei erstellen + filename = os.path.basename(image_path.split('?')[0]) + temp_path = f"/tmp/{filename}" + + with open(temp_path, 'wb') as f: + f.write(response.content) + + media_id = wp_api.upload_media(temp_path, check_duplicate=check_duplicate) + + # Temporäre Datei löschen + os.remove(temp_path) + + return media_id + + except Exception as e: + print(f"Fehler beim Verarbeiten des Bilds von URL: {e}") + return None + else: + # Lokale Datei + if os.path.exists(image_path): + return wp_api.upload_media(image_path, check_duplicate=check_duplicate) + else: + print(f"Bilddatei nicht gefunden: {image_path}") + return None + + +def process_post(wp_api: WordPressAPI, post_config: Dict[str, Any], + global_settings: Dict[str, Any]) -> Optional[int]: + """ + Verarbeitet einen einzelnen Beitrag aus der Konfiguration + + Args: + wp_api: WordPress API Client + post_config: Beitrags-Konfiguration + global_settings: Globale Einstellungen + + Returns: + Post-ID oder None + """ + title = post_config.get('title') + if not title: + print("Fehler: Titel fehlt in der Beitragskonfiguration") + return None + + print(f"\n{'='*60}") + print(f"Verarbeite Beitrag: {title}") + print(f"{'='*60}") + + # Markdown-Inhalt abrufen + markdown_content = None + if 'markdown_url' in post_config: + print(f"Lade Markdown von URL: {post_config['markdown_url']}") + markdown_content = download_markdown(post_config['markdown_url']) + elif 'markdown_file' in post_config: + print(f"Lese lokale Markdown-Datei: {post_config['markdown_file']}") + markdown_content = read_local_markdown(post_config['markdown_file']) + elif 'content' in post_config: + markdown_content = post_config['content'] + + if not markdown_content: + print(f"Fehler: Kein Inhalt für Beitrag '{title}'") + return None + + # Markdown zu HTML konvertieren + extensions = global_settings.get('markdown_extensions', ['extra', 'codehilite', 'toc']) + html_content = markdown_to_html(markdown_content, extensions) + + # Kategorien verarbeiten + category_ids = [] + if 'categories' in post_config: + for cat_name in post_config['categories']: + cat_id = wp_api.get_or_create_category(cat_name) + if cat_id: + category_ids.append(cat_id) + + # Tags verarbeiten + tag_ids = [] + if 'tags' in post_config: + for tag_name in post_config['tags']: + tag_id = wp_api.get_or_create_tag(tag_name) + if tag_id: + tag_ids.append(tag_id) + + # Beitragsbild verarbeiten + featured_media_id = None + if 'featured_image' in post_config: + skip_duplicate_media = global_settings.get('skip_duplicate_media', True) + featured_media_id = process_featured_image( + wp_api, + post_config['featured_image'], + check_duplicate=skip_duplicate_media + ) + + # Status + status = post_config.get('status', global_settings.get('default_status', 'draft')) + + # Excerpt + excerpt = post_config.get('excerpt', '') + + # Beitrag erstellen + skip_duplicates = global_settings.get('skip_duplicates', True) + post_id = wp_api.create_post( + title=title, + content=html_content, + status=status, + featured_media=featured_media_id, + categories=category_ids if category_ids else None, + tags=tag_ids if tag_ids else None, + excerpt=excerpt, + check_duplicate=skip_duplicates + ) + + return post_id + + +def main(): + """Hauptfunktion des Workflows""" + + # Konfigurationsdatei laden + config_file = sys.argv[1] if len(sys.argv) > 1 else 'posts.yaml' + + if not os.path.exists(config_file): + print(f"Fehler: Konfigurationsdatei '{config_file}' nicht gefunden") + print("Verwendung: python workflow.py [config.yaml]") + sys.exit(1) + + print(f"Lade Konfiguration aus: {config_file}") + + with open(config_file, 'r', encoding='utf-8') as f: + config = yaml.safe_load(f) + + # WordPress-Credentials aus Umgebungsvariablen + wp_url = os.getenv('WORDPRESS_URL') + wp_username = os.getenv('WORDPRESS_USERNAME') + wp_password = os.getenv('WORDPRESS_APP_PASSWORD') + + if not all([wp_url, wp_username, wp_password]): + print("Fehler: WordPress-Credentials fehlen in .env-Datei") + print("Benötigt: WORDPRESS_URL, WORDPRESS_USERNAME, WORDPRESS_APP_PASSWORD") + sys.exit(1) + + print(f"\nVerbinde mit WordPress: {wp_url}") + + # WordPress API initialisieren + wp_api = WordPressAPI(wp_url, wp_username, wp_password) + + # Globale Einstellungen + global_settings = config.get('settings', {}) + + # Beiträge verarbeiten + posts = config.get('posts', []) + if not posts: + print("Warnung: Keine Beiträge in der Konfiguration gefunden") + return + + print(f"\nVerarbeite {len(posts)} Beitrag/Beiträge...\n") + + success_count = 0 + error_count = 0 + + for post_config in posts: + try: + post_id = process_post(wp_api, post_config, global_settings) + if post_id: + success_count += 1 + else: + error_count += 1 + except Exception as e: + print(f"Fehler bei der Verarbeitung: {e}") + error_count += 1 + + # Zusammenfassung + print(f"\n{'='*60}") + print(f"ZUSAMMENFASSUNG") + print(f"{'='*60}") + print(f"Erfolgreich: {success_count}") + print(f"Fehler: {error_count}") + print(f"Gesamt: {len(posts)}") + print(f"{'='*60}\n") + + +if __name__ == '__main__': + main()