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