Initial commit: WordPress News Import System
This commit is contained in:
commit
5f923d8ece
8 changed files with 948 additions and 0 deletions
4
.env.example
Normal file
4
.env.example
Normal file
|
|
@ -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
|
||||||
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
|
|
@ -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
|
||||||
281
README.md
Normal file
281
README.md
Normal file
|
|
@ -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="<p>HTML-Inhalt</p>",
|
||||||
|
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.
|
||||||
25
content/beispiel-beitrag.md
Normal file
25
content/beispiel-beitrag.md
Normal file
|
|
@ -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.
|
||||||
50
posts.yaml
Normal file
50
posts.yaml
Normal file
|
|
@ -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
|
||||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
requests>=2.31.0
|
||||||
|
python-dotenv>=1.0.0
|
||||||
|
PyYAML>=6.0.1
|
||||||
|
markdown>=3.5.0
|
||||||
278
wordpress_api.py
Normal file
278
wordpress_api.py
Normal file
|
|
@ -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()
|
||||||
278
workflow.py
Normal file
278
workflow.py
Normal file
|
|
@ -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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue