Merge pull request #12 from OliverGiertz/feature/WP_API

Feature/wp api
This commit is contained in:
Oliver 2025-08-16 13:26:50 +02:00 committed by GitHub
commit 1c63163f22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 7172 additions and 516 deletions

View file

@ -1,3 +1,936 @@
## [v1.6.2] - 2025-08-16
### 🐛 Kritische Fehlerbehebung
- **WordPress-Tag-Upload-Fehler behoben:**
- WordPress REST API benötigt Tag-IDs statt Tag-Namen im `tags`-Parameter
- Neue Funktion `_get_or_create_tags()` ermittelt existierende Tag-IDs oder erstellt neue Tags
- Automatische Tag-Erstellung wenn Tags nicht existieren
- Robuste Fehlerbehandlung für Tag-Verarbeitungsfehler
### 🔧 Verbesserungen
- **Erweiterte Fehleranalyse:**
- Detaillierte Logging-Ausgaben für Post-Daten bei Fehlern
- Spezielle Behandlung von Tag-Parameter-Fehlern
- JSON-formatierte Debug-Ausgaben für bessere Fehleranalyse
- **Tag-Management:**
- Suche nach existierenden Tags mit exakter Namensübereinstimmung
- Automatische Erstellung fehlender Tags über WordPress REST API
- Tag-IDs werden korrekt im Post-Daten-Objekt verwendet
- Leere/ungültige Tags werden übersprungen
### 🛠 Technische Details
- Tag-Verarbeitung erfolgt vor Post-Erstellung
- WordPress `/wp-json/wp/v2/tags` Endpoint für Tag-Management
- Fallback-Verhalten bei Tag-Erstellungsfehlern
- Verbesserte Logging-Ausgaben für Tag-Operationen
---
## [v1.6.1] - 2025-08-16
### 💡 Neue Funktionen
- **WordPress-Integration** implementiert:
- Vollständige WordPress REST API-Anbindung über `utils/wordpress_uploader.py`
- **Base64-Authentifizierung** mit Authorization Header (wie von WordPress API benötigt)
- Neuer Status "WordPress Pending" für hochgeladene Artikel
- Artikel mit Status "Process" können einzeln oder als Batch zu WordPress hochgeladen werden
- Automatische Duplikatserkennung basierend auf Titel-Übereinstimmung
- Meta-Felder werden gesetzt (RSS-Quelle, Original-Link, Import-Datum, RSS-Artikel-ID)
- **Erweiterte UI-Funktionen**:
- Neuer Tab "WordPress" mit Verbindungstest und Konfigurationsübersicht
- WordPress-Upload-Buttons in der Artikel-Übersicht (einzeln und global)
- WordPress-Artikel-Statistiken im Dashboard und Statistiken-Tab
- Detaillierte Upload-Ergebnisse mit Erfolgs-/Fehlerstatistiken
- Debug-Modus für Auth-Details (Entwicklung)
- **Verbesserte Artikel-Verwaltung**:
- WordPress Post ID und Upload-Datum werden in Artikeln gespeichert
- Status-Workflow: New → Rewrite → Process → WordPress Pending → Online
- Anzeige von WordPress-Informationen in der Artikel-Detailansicht
### 🔧 Verbesserungen
- **Korrekte WordPress-API-Authentifizierung**:
- Unterstützung für bereitgestellten Base64-Auth-String (`WP_AUTH_BASE64`)
- Fallback auf automatische Base64-Generierung aus Username/Password
- Authorization Header im korrekten Format: `Basic <base64_credentials>`
- Erweiterte Debug-Ausgaben für Authentifizierung
- **Robuste Fehlerbehandlung**:
- Ausführliches Logging für alle WordPress-Operationen inkl. Auth-Details
- Retry-Mechanismus mit exponential backoff bei Netzwerkfehlern
- Detaillierte Fehlermeldungen für verschiedene HTTP-Status-Codes (401, 403, etc.)
- Verbindungstest vor Upload-Operationen mit Auth-Verifikation
- **Erweiterte WordPress-API-Funktionen**:
- Automatische Ermittlung der Standard-Kategorie "Allgemein"
- Session-basierte HTTP-Verbindungen für bessere Performance
- Unterstützung für WordPress-Meta-Felder zur Nachverfolgung
- Berücksichtigung verschiedener WordPress-Authentifizierungsfehler
- **UI/UX-Verbesserungen**:
- Neuer Status-Badge für "WordPress Pending" mit eigenem Styling
- Dashboard zeigt WordPress-spezifische Statistiken
- Konfigurationshilfen und .env-Vorlagen im WordPress-Tab
- Massenupload-Funktionalität mit Progress-Feedback
- Base64-Auth-Status in Konfigurationsübersicht
### 🛠 Interne Änderungen
- `main.py` erweitert um `upload_articles_to_wp()` Funktion
- `VALID_STATUSES` um "WordPress Pending" erweitert
- Neue Umgebungsvariable `WP_AUTH_BASE64` für direkte Base64-Authentifizierung
- Erweiterte Artikel-Datenstruktur um WordPress-spezifische Felder
- Session-Management für HTTP-Verbindungen implementiert
- Base64-Authentifizierung mit Fallback-Mechanismus
### 📁 Neue Dateien
- `utils/wordpress_uploader.py` - Vollständige WordPress REST API-Integration mit Base64-Auth
- Erweiterte `.env`-Vorlage mit WordPress-Konfiguration inkl. Base64-String
### 🔒 Sicherheit
- WordPress-Credentials werden sicher über Umgebungsvariablen verwaltet
- Base64-Auth über Anwendungspasswort (sicherer als Haupt-Login)
- Keine sensiblen Daten in Logs oder Fehlermeldungen
- Authorization Header im WordPress-Standard-Format
### 📋 Authentifizierungs-Setup
**Bereitgestellte Konfiguration:**
```bash
WP_AUTH_BASE64=b2dpZXJ0ejp3aE5FeDlhWkNJVVhWaVY4OVozZTdaMDM=
# Dekodiert: ogiertz:whNEx9aZCIUXViV89Z3e7Z03
```
**Authorization Header:**
```
Authorization: Basic b2dpZXJ0ejp3aE5FeDlhWkNJVVhWaVY4OVozZTdaMDM=
```
---
## [v1.5.3] - 2025-07-11
### 💡 Neue Funktionen
- **WordPress-Integration** implementiert:
- Vollständige WordPress REST API-Anbindung über `utils/wordpress_uploader.py`
- Neuer Status "WordPress Pending" für hochgeladene Artikel
- Artikel mit Status "Process" können einzeln oder als Batch zu WordPress hochgeladen werden
- Automatische Duplikatserkennung basierend auf Titel-Übereinstimmung
- Meta-Felder werden gesetzt (RSS-Quelle, Original-Link, Import-Datum, RSS-Artikel-ID)
- **Erweiterte UI-Funktionen**:
- Neuer Tab "WordPress" mit Verbindungstest und Konfigurationsübersicht
- WordPress-Upload-Buttons in der Artikel-Übersicht (einzeln und global)
- WordPress-Artikel-Statistiken im Dashboard und Statistiken-Tab
- Detaillierte Upload-Ergebnisse mit Erfolgs-/Fehlerstatistiken
- **Verbesserte Artikel-Verwaltung**:
- WordPress Post ID und Upload-Datum werden in Artikeln gespeichert
- Status-Workflow: New → Rewrite → Process → WordPress Pending → Online
- Anzeige von WordPress-Informationen in der Artikel-Detailansicht
### 🔧 Verbesserungen
- **Robuste Fehlerbehandlung**:
- Ausführliches Logging für alle WordPress-Operationen
- Retry-Mechanismus mit exponential backoff bei Netzwerkfehlern
- Detaillierte Fehlermeldungen für verschiedene HTTP-Status-Codes
- Verbindungstest vor Upload-Operationen
- **Erweiterte WordPress-API-Funktionen**:
- Automatische Ermittlung der Standard-Kategorie "Allgemein"
- Session-basierte HTTP-Verbindungen für bessere Performance
- Unterstützung für WordPress-Meta-Felder zur Nachverfolgung
- Berücksichtigung verschiedener WordPress-Authentifizierungsfehler
- **UI/UX-Verbesserungen**:
- Neuer Status-Badge für "WordPress Pending" mit eigenem Styling
- Dashboard zeigt WordPress-spezifische Statistiken
- Konfigurationshilfen und .env-Vorlagen im WordPress-Tab
- Massenupload-Funktionalität mit Progress-Feedback
### 🛠 Interne Änderungen
- `main.py` erweitert um `upload_articles_to_wp()` Funktion
- `VALID_STATUSES` um "WordPress Pending" erweitert
- Neue Umgebungsvariablen für WordPress-Konfiguration
- Erweiterte Artikel-Datenstruktur um WordPress-spezifische Felder
- Session-Management für HTTP-Verbindungen implementiert
### 📁 Neue Dateien
- `utils/wordpress_uploader.py` - Vollständige WordPress REST API-Integration
- Erweiterte `.env`-Vorlage mit WordPress-Konfiguration
### 🔒 Sicherheit
- WordPress-Credentials werden sicher über Umgebungsvariablen verwaltet
- Basic Auth über Anwendungspasswort (sicherer als Haupt-Login)
- Keine sensiblen Daten in Logs oder Fehlermeldungen
---
## [v1.5.3] - 2025-07-11
### ✨ Neue Funktionen
- Automatischer Volltextabruf bei zu kurzen Artikeln (< 50 Wörter)
- Inhalte werden direkt von der Originalseite geladen (ähnlich wie bei der Bildextraktion)
- Promobil, Camping-News und andere gängige WordPress-Seiten werden unterstützt
- Neue Verwaltungsseite `Feed-Verwaltung` unter `pages/01_feed_manager.py`
- RSS-Feeds können nun über eine dedizierte Oberfläche hinzugefügt, bearbeitet und gelöscht werden
- Anzahl verknüpfter Artikel pro Feed wird angezeigt
- Änderungen werden protokolliert und per `st.rerun()` sofort sichtbar
### 🔧 Verbesserungen
- Feed-Filter in der Artikelübersicht zeigt jetzt die **korrekten Feed-Namen mit Artikelanzahl**
- Beispiel: „Promobil News (12)" statt nur „Alle (20)"
- Basierend auf `source`-Feld im Artikelobjekt
- Verbesserte Logging-Ausgaben bei Feed-Aktionen (hinzufügen, ändern, löschen)
### 📁 Neue Dateien
- `utils/article_extractor.py` Logik zum Abrufen vollständiger Artikeltexte von Originalseiten
- `pages/01_feed_manager.py` Eigenständige Verwaltungsseite für RSS-Feeds
### 🛠 Interne Änderungen
- `main.py` erweitert: Automatischer Fallback auf `extract_full_article()` bei zu kurzem Text
- Logging konsolidiert und mit Feed-Aktionen ergänzt
## [v1.5.2] - 2025-07-09
- Fehlerbehandlung bei `CHANGELOG.md`-Doppelungen hinzugefügt
- Signaturlogik robuster (SSH, GPG, fallback)
- Farbige Terminalausgabe verbessert
- dry-run Argument hinzugefügt:
* Versionsnummer wird berechnet ✅
* Änderungen (Version, Changelog, Commit, Tag, Push) werden nur angezeigt, nicht ausgeführt ✅
* Ausgabe erfolgt farbig und klar gegliedert ✅
## [1.5.1] - 2025-07-09
SSH-Commit-Signatur in versioning.py eingebaut
Automatischer Fallback auf GPG oder keine Signatur
Farbige Terminalausgabe zur Signaturmethode
Readme erweitert mit Setup-Anleitung
## [v1.5.0] 2025-07-08
### 💡 Neue Funktionen
- 🪄 DALL·E-Bildgenerierung per Button direkt im Artikel-Expander
- Automatische Metadaten (Caption, Copyright, Quelle) für KI-generierte Bilder
### 🔧 Änderungen & Fixes
- 🔒 Kritischer Bugfix: Artikel gingen nach DALL·E oder Rewrite verloren → jetzt sichere `save_articles()`-Logik über alle Artikel
- Status-Änderungen, Rewrite und Bilderfassung überschreiben nicht mehr die Gesamtdatei
- Kein `st.rerun()` mehr nach jedem Klick flüssiger Workflow
### 📦 Internes
- Neue Datei `utils/dalle_generator.py` für DALL·E-Integration
- Erweiterung der Teststrategie um strukturierte `TEST-CHECKLIST.md`
- Verbesserte Update-Strategie für Einzelartikel bei Bearbeitung
## [v1.4.8] 2025-07-07
### 💡 Neue Funktionen
-
### 🔧 Änderungen & Fixes
- Fehlerbehebung bei neuen Release, CHANGELOG wurde nicht angehangen, es wird nun die gesamte Datei übernommen
### 📦 Internes
-
## [v1.4.7] 2025-07-07
### 💡 Neue Funktionen
- Automatischer Release-Workflow bei `git tag v*`
- Release-Text aus `CHANGELOG.md` wird extrahiert und als GitHub Release verwendet
### 🔧 Änderungen & Fixes
- Fehlerbehebung bei neuen Release, CHANGELOG wurde nicht angehangen
### 📦 Internes
- Erweiterte `release.yml` zur zuverlässigen Release-Erstellung
- GitHub Actions mit `softprops/action-gh-release`
## [v1.4.6] 2025-07-07
### 💡 Neue Funktionen
- Automatischer Release-Workflow bei `git tag v*`
- Release-Text aus `CHANGELOG.md` wird extrahiert und als GitHub Release verwendet
### 🔧 Änderungen & Fixes
- Fehlerbehebung bei Bilddatenextraktion
- Erweiterung von `versioning.py` um automatische Tag-Erstellung und Push
### 📦 Internes
- Erweiterte `release.yml` zur zuverlässigen Release-Erstellung
- GitHub Actions mit `softprops/action-gh-release`
## [v1.4.5] 2025-07-07
### 💡 Neue Funktionen
- Umstellung des versioning.py-Skripts auf eine moderne Typer-CLI:
- create zum Erstellen neuer Versionen mit Level und Push-Option
- rollback zum Zurücknehmen der letzten Version
- list zur Anzeige aller Versionen im CHANGELOG.md
- Validierung, ob der CHANGELOG.md-Eintrag vor Release wirklich ausgefüllt wurde
- Interaktive CLI-Prompts zur besseren Benutzerführung
### 🔧 Änderungen & Fixes
- versioning.py ersetzt bisherige manuelle Menüs durch Typer-Kommandos
- requirements.txt um typer[all]==0.12.3 ergänzt
### 📦 Internes
- Vorbereitung für globale CLI-Nutzung (versioning als Befehl möglich)
- Automatisierung des Release-Prozesses mit GitHub Actions weiterhin vorbereitet
## [v1.4.4] 2025-07-07
### 💡 Neue Funktionen
-
### 🔧 Änderungen & Fixes
-
### 📦 Internes
- automatische Versionierung eingebunden und direktes GitHub puschen der Änderungen
- ## [v1.4.3] 2025-07-07
### 💡 Neue Funktionen
- ⚠️ Visuelle Warnanzeige in der Artikeltabelle für unvollständige Bildmetadaten (fehlende Caption, Copyright oder Quelle)
- ✍️ Inline-Bearbeitung von Bilddaten (Caption, Copyright, Quelle) direkt in der Detailansicht
- 🪵 Neue separate Seite `Log-Viewer` zur Anzeige der letzten Log-Einträge (automatisch über `pages/log_viewer.py`)
- 📂 Startfilter für Artikelansicht auf „New" voreingestellt für fokussierten Workflow
### 🔧 Änderungen & Fixes
- ✅ Artikel aus Feeds überschreiben bestehende Artikel **nicht mehr** Status, Tags und andere manuelle Änderungen bleiben erhalten
- 🧹 `get_recent_logs()` wurde entfernt und die Sidebar-Logausgabe aus `app.py` entfernt
- 🔗 Sidebar-Link zur Log-Seite hinzugefügt (mittlerweile durch native Seiten-Navigation ersetzt)
- 🧭 Navigation durch Nutzung von Streamlit-Multipage-Struktur (`pages/`)
### 📦 Internes
- Refactoring von `process_articles()` zur sicheren ID-basierten Artikelzusammenführung
- Verbesserte Logging-Ausgabe bei bereits vorhandenen Artikeln
- Robusteres Fehlerhandling in `image_extractor.py`
## [v1.4.2] 2025-07-03
### 💡 Neue Funktionen
- Komplett überarbeitete Artikel-Tabelle mit:
- Auswahlcheckboxen
- Inline-Statuswechsel mit Dropdown
- Wortanzahl, Tag-Anzeige, Datum kompakt
- Copy-to-Clipboard Funktion für Titel, Text und Tags
- Bildanzeige inkl. Caption und Copyright-Quelle im Detailbereich
- Titel wird automatisch beim Kopieren des Texts vorangestellt
### 🔧 Änderungen & Fixes
- `st.experimental_rerun()` durch `st.rerun()` ersetzt
- Statusfilter „Alle" funktioniert jetzt korrekt
- UI-Tuning für bessere Lesbarkeit
- Feedliste aus der Sidebar entfernt
- Fix: Bilddaten ohne Caption verursachen keine Fehler mehr
- Artikelüberschriften korrekt in Kopiertext eingebaut
### 📦 Internes
- Logging bleibt aktiv im Verzeichnis `/logs`
- Vorbereitung für Bildquellen-Import aus Original-Artikel umgesetzt
## [1.4.1] 2025-07-03
### Hinzugefügt
- Logging für `process_articles()`, damit nachvollziehbar ist, welche Feeds verarbeitet wurden
- Rückmeldung in der App bei Klick auf „Alle Feeds neu laden"
### Geändert
- `main.py`: Inhalte aus `content`, `summary` oder `description` werden vollständig geladen und mit `BeautifulSoup` bereinigt
- Sicherstellung, dass `fetch_and_process_feed()` alle relevanten Artikelinformationen vollständig speichert
### Fehlerbehebungen
- Problem behoben, bei dem Artikeltexte nicht vollständig übernommen wurden
## [1.3.1] 2025-07-03
### Added
- Tabellenansicht mit Checkbox, Titel, Datum, Zusammenfassung, Wortanzahl, Tags, Status
- Direktes Bearbeiten des Status über Dropdown-Menü
- Massenbearbeitung von Artikeln per Checkbox
- Rewrite-Button für alle Artikel mit Status 'Rewrite'
## [1.2.0] - 2025-07-04
### Hinzugefügt
- Automatische Bilderkennung beim Einlesen von Artikeln
- Extrahieren von Bildern aus dem Originalartikel (bis zu 3 Bilder)
- Speicherung von Bild-URLs, Alt-Texten (Bildbeschreibung) und Copyright-Hinweisen
- Fehlerbehandlung für nicht erreichbare Seiten
- Darstellung der Bilder (inkl. Beschreibung & Copyright) in der Artikelansicht
### Geändert
- Bilder werden direkt beim Einlesen eines RSS-Artikels verarbeitet und gespeichert
- `app.py` zeigt nun auch Bildinformationen innerhalb der Artikeldetailansicht an
### Behoben
- Keine
---
## [1.1.0] - 2025-07-04
### Hinzugefügt
- Visuell aufgewertete Box zur Darstellung eines Artikels mit:
- Kopierbutton für Titel
- Kopierbutton für Artikeltext
- Kopierbutton für Tags
- Button zum Öffnen des Originalartikels im neuen Tab
- Artikelansicht ist nun in einer grauen, abgerundeten Box gekapselt
- Icons unterstützen visuelle Orientierung (📝, 🗌, 📌 etc.)
### Geändert
- Artikelkopierfunktion für WordPress ist nun interaktiv über Buttons möglich
- HTML-Markup innerhalb von Streamlit für flexibleres Styling
### Behoben
- Keine
---
## [1.0.0] - 2025-07-03
### Initialversion
- Artikel aus RSS-Feeds einlesen
- Speichern in JSON-Datei
- Anzeige in Tabelle mit Statusfilter
- Rewrite per ChatGPT mit Zusammenfassung und Tag-Generierung
- Exportierbare Inhalte für manuelles Posting auf WordPress
---
## [v1.6.1] - 2025-08-16
### 💡 Neue Funktionen
- **WordPress-Integration** implementiert:
- Vollständige WordPress REST API-Anbindung über `utils/wordpress_uploader.py`
- **Base64-Authentifizierung** mit Authorization Header (wie von WordPress API benötigt)
- Neuer Status "WordPress Pending" für hochgeladene Artikel
- Artikel mit Status "Process" können einzeln oder als Batch zu WordPress hochgeladen werden
- Automatische Duplikatserkennung basierend auf Titel-Übereinstimmung
- Meta-Felder werden gesetzt (RSS-Quelle, Original-Link, Import-Datum, RSS-Artikel-ID)
- **Erweiterte UI-Funktionen**:
- Neuer Tab "WordPress" mit Verbindungstest und Konfigurationsübersicht
- WordPress-Upload-Buttons in der Artikel-Übersicht (einzeln und global)
- WordPress-Artikel-Statistiken im Dashboard und Statistiken-Tab
- Detaillierte Upload-Ergebnisse mit Erfolgs-/Fehlerstatistiken
- Debug-Modus für Auth-Details (Entwicklung)
- **Verbesserte Artikel-Verwaltung**:
- WordPress Post ID und Upload-Datum werden in Artikeln gespeichert
- Status-Workflow: New → Rewrite → Process → WordPress Pending → Online
- Anzeige von WordPress-Informationen in der Artikel-Detailansicht
### 🔧 Verbesserungen
- **Korrekte WordPress-API-Authentifizierung**:
- Unterstützung für bereitgestellten Base64-Auth-String (`WP_AUTH_BASE64`)
- Fallback auf automatische Base64-Generierung aus Username/Password
- Authorization Header im korrekten Format: `Basic <base64_credentials>`
- Erweiterte Debug-Ausgaben für Authentifizierung
- **Robuste Fehlerbehandlung**:
- Ausführliches Logging für alle WordPress-Operationen inkl. Auth-Details
- Retry-Mechanismus mit exponential backoff bei Netzwerkfehlern
- Detaillierte Fehlermeldungen für verschiedene HTTP-Status-Codes (401, 403, etc.)
- Verbindungstest vor Upload-Operationen mit Auth-Verifikation
- **Erweiterte WordPress-API-Funktionen**:
- Automatische Ermittlung der Standard-Kategorie "Allgemein"
- Session-basierte HTTP-Verbindungen für bessere Performance
- Unterstützung für WordPress-Meta-Felder zur Nachverfolgung
- Berücksichtigung verschiedener WordPress-Authentifizierungsfehler
- **UI/UX-Verbesserungen**:
- Neuer Status-Badge für "WordPress Pending" mit eigenem Styling
- Dashboard zeigt WordPress-spezifische Statistiken
- Konfigurationshilfen und .env-Vorlagen im WordPress-Tab
- Massenupload-Funktionalität mit Progress-Feedback
- Base64-Auth-Status in Konfigurationsübersicht
### 🛠 Interne Änderungen
- `main.py` erweitert um `upload_articles_to_wp()` Funktion
- `VALID_STATUSES` um "WordPress Pending" erweitert
- Neue Umgebungsvariable `WP_AUTH_BASE64` für direkte Base64-Authentifizierung
- Erweiterte Artikel-Datenstruktur um WordPress-spezifische Felder
- Session-Management für HTTP-Verbindungen implementiert
- Base64-Authentifizierung mit Fallback-Mechanismus
### 📁 Neue Dateien
- `utils/wordpress_uploader.py` - Vollständige WordPress REST API-Integration mit Base64-Auth
- Erweiterte `.env`-Vorlage mit WordPress-Konfiguration inkl. Base64-String
### 🔒 Sicherheit
- WordPress-Credentials werden sicher über Umgebungsvariablen verwaltet
- Base64-Auth über Anwendungspasswort (sicherer als Haupt-Login)
- Keine sensiblen Daten in Logs oder Fehlermeldungen
- Authorization Header im WordPress-Standard-Format
### 📋 Authentifizierungs-Setup
**Bereitgestellte Konfiguration:**
```bash
WP_AUTH_BASE64=b2dpZXJ0ejp3aE5FeDlhWkNJVVhWaVY4OVozZTdaMDM=
# Dekodiert: ogiertz:whNEx9aZCIUXViV89Z3e7Z03
```
**Authorization Header:**
```
Authorization: Basic b2dpZXJ0ejp3aE5FeDlhWkNJVVhWaVY4OVozZTdaMDM=
```
---
## [v1.5.3] - 2025-07-11
### 💡 Neue Funktionen
- **WordPress-Integration** implementiert:
- Vollständige WordPress REST API-Anbindung über `utils/wordpress_uploader.py`
- Neuer Status "WordPress Pending" für hochgeladene Artikel
- Artikel mit Status "Process" können einzeln oder als Batch zu WordPress hochgeladen werden
- Automatische Duplikatserkennung basierend auf Titel-Übereinstimmung
- Meta-Felder werden gesetzt (RSS-Quelle, Original-Link, Import-Datum, RSS-Artikel-ID)
- **Erweiterte UI-Funktionen**:
- Neuer Tab "WordPress" mit Verbindungstest und Konfigurationsübersicht
- WordPress-Upload-Buttons in der Artikel-Übersicht (einzeln und global)
- WordPress-Artikel-Statistiken im Dashboard und Statistiken-Tab
- Detaillierte Upload-Ergebnisse mit Erfolgs-/Fehlerstatistiken
- **Verbesserte Artikel-Verwaltung**:
- WordPress Post ID und Upload-Datum werden in Artikeln gespeichert
- Status-Workflow: New → Rewrite → Process → WordPress Pending → Online
- Anzeige von WordPress-Informationen in der Artikel-Detailansicht
### 🔧 Verbesserungen
- **Robuste Fehlerbehandlung**:
- Ausführliches Logging für alle WordPress-Operationen
- Retry-Mechanismus mit exponential backoff bei Netzwerkfehlern
- Detaillierte Fehlermeldungen für verschiedene HTTP-Status-Codes
- Verbindungstest vor Upload-Operationen
- **Erweiterte WordPress-API-Funktionen**:
- Automatische Ermittlung der Standard-Kategorie "Allgemein"
- Session-basierte HTTP-Verbindungen für bessere Performance
- Unterstützung für WordPress-Meta-Felder zur Nachverfolgung
- Berücksichtigung verschiedener WordPress-Authentifizierungsfehler
- **UI/UX-Verbesserungen**:
- Neuer Status-Badge für "WordPress Pending" mit eigenem Styling
- Dashboard zeigt WordPress-spezifische Statistiken
- Konfigurationshilfen und .env-Vorlagen im WordPress-Tab
- Massenupload-Funktionalität mit Progress-Feedback
### 🛠 Interne Änderungen
- `main.py` erweitert um `upload_articles_to_wp()` Funktion
- `VALID_STATUSES` um "WordPress Pending" erweitert
- Neue Umgebungsvariablen für WordPress-Konfiguration
- Erweiterte Artikel-Datenstruktur um WordPress-spezifische Felder
- Session-Management für HTTP-Verbindungen implementiert
### 📁 Neue Dateien
- `utils/wordpress_uploader.py` - Vollständige WordPress REST API-Integration
- Erweiterte `.env`-Vorlage mit WordPress-Konfiguration
### 🔒 Sicherheit
- WordPress-Credentials werden sicher über Umgebungsvariablen verwaltet
- Basic Auth über Anwendungspasswort (sicherer als Haupt-Login)
- Keine sensiblen Daten in Logs oder Fehlermeldungen
---
## [v1.5.3] - 2025-07-11
### ✨ Neue Funktionen
- Automatischer Volltextabruf bei zu kurzen Artikeln (< 50 Wörter)
- Inhalte werden direkt von der Originalseite geladen (ähnlich wie bei der Bildextraktion)
- Promobil, Camping-News und andere gängige WordPress-Seiten werden unterstützt
- Neue Verwaltungsseite `Feed-Verwaltung` unter `pages/01_feed_manager.py`
- RSS-Feeds können nun über eine dedizierte Oberfläche hinzugefügt, bearbeitet und gelöscht werden
- Anzahl verknüpfter Artikel pro Feed wird angezeigt
- Änderungen werden protokolliert und per `st.rerun()` sofort sichtbar
### 🔧 Verbesserungen
- Feed-Filter in der Artikelübersicht zeigt jetzt die **korrekten Feed-Namen mit Artikelanzahl**
- Beispiel: „Promobil News (12)" statt nur „Alle (20)"
- Basierend auf `source`-Feld im Artikelobjekt
- Verbesserte Logging-Ausgaben bei Feed-Aktionen (hinzufügen, ändern, löschen)
### 📁 Neue Dateien
- `utils/article_extractor.py` Logik zum Abrufen vollständiger Artikeltexte von Originalseiten
- `pages/01_feed_manager.py` Eigenständige Verwaltungsseite für RSS-Feeds
### 🛠 Interne Änderungen
- `main.py` erweitert: Automatischer Fallback auf `extract_full_article()` bei zu kurzem Text
- Logging konsolidiert und mit Feed-Aktionen ergänzt
## [v1.5.2] - 2025-07-09
- Fehlerbehandlung bei `CHANGELOG.md`-Doppelungen hinzugefügt
- Signaturlogik robuster (SSH, GPG, fallback)
- Farbige Terminalausgabe verbessert
- dry-run Argument hinzugefügt:
* Versionsnummer wird berechnet ✅
* Änderungen (Version, Changelog, Commit, Tag, Push) werden nur angezeigt, nicht ausgeführt ✅
* Ausgabe erfolgt farbig und klar gegliedert ✅
## [1.5.1] - 2025-07-09
SSH-Commit-Signatur in versioning.py eingebaut
Automatischer Fallback auf GPG oder keine Signatur
Farbige Terminalausgabe zur Signaturmethode
Readme erweitert mit Setup-Anleitung
## [v1.5.0] 2025-07-08
### 💡 Neue Funktionen
- 🪄 DALL·E-Bildgenerierung per Button direkt im Artikel-Expander
- Automatische Metadaten (Caption, Copyright, Quelle) für KI-generierte Bilder
### 🔧 Änderungen & Fixes
- 🔒 Kritischer Bugfix: Artikel gingen nach DALL·E oder Rewrite verloren → jetzt sichere `save_articles()`-Logik über alle Artikel
- Status-Änderungen, Rewrite und Bilderfassung überschreiben nicht mehr die Gesamtdatei
- Kein `st.rerun()` mehr nach jedem Klick flüssiger Workflow
### 📦 Internes
- Neue Datei `utils/dalle_generator.py` für DALL·E-Integration
- Erweiterung der Teststrategie um strukturierte `TEST-CHECKLIST.md`
- Verbesserte Update-Strategie für Einzelartikel bei Bearbeitung
## [v1.4.8] 2025-07-07
### 💡 Neue Funktionen
-
### 🔧 Änderungen & Fixes
- Fehlerbehebung bei neuen Release, CHANGELOG wurde nicht angehangen, es wird nun die gesamte Datei übernommen
### 📦 Internes
-
## [v1.4.7] 2025-07-07
### 💡 Neue Funktionen
- Automatischer Release-Workflow bei `git tag v*`
- Release-Text aus `CHANGELOG.md` wird extrahiert und als GitHub Release verwendet
### 🔧 Änderungen & Fixes
- Fehlerbehebung bei neuen Release, CHANGELOG wurde nicht angehangen
### 📦 Internes
- Erweiterte `release.yml` zur zuverlässigen Release-Erstellung
- GitHub Actions mit `softprops/action-gh-release`
## [v1.4.6] 2025-07-07
### 💡 Neue Funktionen
- Automatischer Release-Workflow bei `git tag v*`
- Release-Text aus `CHANGELOG.md` wird extrahiert und als GitHub Release verwendet
### 🔧 Änderungen & Fixes
- Fehlerbehebung bei Bilddatenextraktion
- Erweiterung von `versioning.py` um automatische Tag-Erstellung und Push
### 📦 Internes
- Erweiterte `release.yml` zur zuverlässigen Release-Erstellung
- GitHub Actions mit `softprops/action-gh-release`
## [v1.4.5] 2025-07-07
### 💡 Neue Funktionen
- Umstellung des versioning.py-Skripts auf eine moderne Typer-CLI:
- create zum Erstellen neuer Versionen mit Level und Push-Option
- rollback zum Zurücknehmen der letzten Version
- list zur Anzeige aller Versionen im CHANGELOG.md
- Validierung, ob der CHANGELOG.md-Eintrag vor Release wirklich ausgefüllt wurde
- Interaktive CLI-Prompts zur besseren Benutzerführung
### 🔧 Änderungen & Fixes
- versioning.py ersetzt bisherige manuelle Menüs durch Typer-Kommandos
- requirements.txt um typer[all]==0.12.3 ergänzt
### 📦 Internes
- Vorbereitung für globale CLI-Nutzung (versioning als Befehl möglich)
- Automatisierung des Release-Prozesses mit GitHub Actions weiterhin vorbereitet
## [v1.4.4] 2025-07-07
### 💡 Neue Funktionen
-
### 🔧 Änderungen & Fixes
-
### 📦 Internes
- automatische Versionierung eingebunden und direktes GitHub puschen der Änderungen
- ## [v1.4.3] 2025-07-07
### 💡 Neue Funktionen
- ⚠️ Visuelle Warnanzeige in der Artikeltabelle für unvollständige Bildmetadaten (fehlende Caption, Copyright oder Quelle)
- ✍️ Inline-Bearbeitung von Bilddaten (Caption, Copyright, Quelle) direkt in der Detailansicht
- 🪵 Neue separate Seite `Log-Viewer` zur Anzeige der letzten Log-Einträge (automatisch über `pages/log_viewer.py`)
- 📂 Startfilter für Artikelansicht auf „New" voreingestellt für fokussierten Workflow
### 🔧 Änderungen & Fixes
- ✅ Artikel aus Feeds überschreiben bestehende Artikel **nicht mehr** Status, Tags und andere manuelle Änderungen bleiben erhalten
- 🧹 `get_recent_logs()` wurde entfernt und die Sidebar-Logausgabe aus `app.py` entfernt
- 🔗 Sidebar-Link zur Log-Seite hinzugefügt (mittlerweile durch native Seiten-Navigation ersetzt)
- 🧭 Navigation durch Nutzung von Streamlit-Multipage-Struktur (`pages/`)
### 📦 Internes
- Refactoring von `process_articles()` zur sicheren ID-basierten Artikelzusammenführung
- Verbesserte Logging-Ausgabe bei bereits vorhandenen Artikeln
- Robusteres Fehlerhandling in `image_extractor.py`
## [v1.4.2] 2025-07-03
### 💡 Neue Funktionen
- Komplett überarbeitete Artikel-Tabelle mit:
- Auswahlcheckboxen
- Inline-Statuswechsel mit Dropdown
- Wortanzahl, Tag-Anzeige, Datum kompakt
- Copy-to-Clipboard Funktion für Titel, Text und Tags
- Bildanzeige inkl. Caption und Copyright-Quelle im Detailbereich
- Titel wird automatisch beim Kopieren des Texts vorangestellt
### 🔧 Änderungen & Fixes
- `st.experimental_rerun()` durch `st.rerun()` ersetzt
- Statusfilter „Alle" funktioniert jetzt korrekt
- UI-Tuning für bessere Lesbarkeit
- Feedliste aus der Sidebar entfernt
- Fix: Bilddaten ohne Caption verursachen keine Fehler mehr
- Artikelüberschriften korrekt in Kopiertext eingebaut
### 📦 Internes
- Logging bleibt aktiv im Verzeichnis `/logs`
- Vorbereitung für Bildquellen-Import aus Original-Artikel umgesetzt
## [1.4.1] 2025-07-03
### Hinzugefügt
- Logging für `process_articles()`, damit nachvollziehbar ist, welche Feeds verarbeitet wurden
- Rückmeldung in der App bei Klick auf „Alle Feeds neu laden"
### Geändert
- `main.py`: Inhalte aus `content`, `summary` oder `description` werden vollständig geladen und mit `BeautifulSoup` bereinigt
- Sicherstellung, dass `fetch_and_process_feed()` alle relevanten Artikelinformationen vollständig speichert
### Fehlerbehebungen
- Problem behoben, bei dem Artikeltexte nicht vollständig übernommen wurden
## [1.3.1] 2025-07-03
### Added
- Tabellenansicht mit Checkbox, Titel, Datum, Zusammenfassung, Wortanzahl, Tags, Status
- Direktes Bearbeiten des Status über Dropdown-Menü
- Massenbearbeitung von Artikeln per Checkbox
- Rewrite-Button für alle Artikel mit Status 'Rewrite'
## [1.2.0] - 2025-07-04
### Hinzugefügt
- Automatische Bilderkennung beim Einlesen von Artikeln
- Extrahieren von Bildern aus dem Originalartikel (bis zu 3 Bilder)
- Speicherung von Bild-URLs, Alt-Texten (Bildbeschreibung) und Copyright-Hinweisen
- Fehlerbehandlung für nicht erreichbare Seiten
- Darstellung der Bilder (inkl. Beschreibung & Copyright) in der Artikelansicht
### Geändert
- Bilder werden direkt beim Einlesen eines RSS-Artikels verarbeitet und gespeichert
- `app.py` zeigt nun auch Bildinformationen innerhalb der Artikeldetailansicht an
### Behoben
- Keine
---
## [1.1.0] - 2025-07-04
### Hinzugefügt
- Visuell aufgewertete Box zur Darstellung eines Artikels mit:
- Kopierbutton für Titel
- Kopierbutton für Artikeltext
- Kopierbutton für Tags
- Button zum Öffnen des Originalartikels im neuen Tab
- Artikelansicht ist nun in einer grauen, abgerundeten Box gekapselt
- Icons unterstützen visuelle Orientierung (📝, 🗌, 📌 etc.)
### Geändert
- Artikelkopierfunktion für WordPress ist nun interaktiv über Buttons möglich
- HTML-Markup innerhalb von Streamlit für flexibleres Styling
### Behoben
- Keine
---
## [1.0.0] - 2025-07-03
### Initialversion
- Artikel aus RSS-Feeds einlesen
- Speichern in JSON-Datei
- Anzeige in Tabelle mit Statusfilter
- Rewrite per ChatGPT mit Zusammenfassung und Tag-Generierung
- Exportierbare Inhalte für manuelles Posting auf WordPress
----
## [v1.6.0] - 2025-08-15
### 🎨 Komplette UI-Überarbeitung
- **Modernes Tab-basiertes Design** mit Dashboard, Artikel, Feeds, Bilder und Statistiken-Tabs
- **Card-basierte Artikelansicht** ersetzt die alte Tabellenstruktur
- **Gradient-Header** und moderne CSS-Styling für professionelleres Aussehen
- **Responsive Layout** mit verbesserter mobiler Darstellung
- **Status-Badges** mit farbkodierten Indikatoren
- **Toast-Benachrichtigungen** für besseres User-Feedback
### 🔍 Erweiterte Filter- und Suchfunktionen
- **Kombinierte Filter** für Status, Feed und Volltextsuche
- **Live-Suche** durch Titel, Inhalt und Tags
- **Feed-spezifische Filterung** mit Artikelanzahl-Anzeige
- **Session State Management** für persistente Filter-Einstellungen
### 📊 Neues Dashboard
- **Statistik-Karten** mit visuellen Metriken (Gesamt-Artikel, neue Artikel, Feeds, Online-Artikel)
- **Schnellaktionen** für häufige Aufgaben (Feed-Update, Rewrite, Aufräumen)
- **Neueste Artikel Preview** mit Status-Anzeige
- **Übersichtliche Zahlen** mit modernem Design
### 🖼️ Verbesserte Bildverwaltung
- **Dedizierte Bilder-Seite** mit Galerie-Ansicht
- **Erweiterte Bildextraktion** mit Featured Image Detection
- **OpenGraph und Twitter Card** Unterstützung
- **Intelligente Bildfilterung** (Größe, Typ, Blacklist)
- **Metadaten-Bereinigung** mit Fallback-Werten
### 📰 Optimierte Artikelverarbeitung
- **Erweiterte Duplikatserkennung** basierend auf Titel-Ähnlichkeit und URL
- **Verbesserte Volltextextraktion** mit website-spezifischen Selektoren
- **WordPress-Erkennung** für optimierte Content-Extraktion
- **Retry-Mechanismus** mit exponential backoff
- **Bessere Textbereinigung** und Validierung
### 🛠️ Backend-Verbesserungen
- **Strukturiertes Logging** mit Funktions- und Zeilennummern
- **Session State Management** für bessere Performance
- **Verbesserte Fehlerbehandlung** mit spezifischen Error-Messages
- **JSON-Validierung** vor dem Speichern
- **Encoding-Fixes** für internationale Zeichen
- **Memory-optimierte Verarbeitung**
### 📊 Neue Statistiken-Seite
- **Status-Verteilung** mit Prozentanzeigen
- **Feed-Artikel-Übersicht** sortiert nach Anzahl
- **Textstatistiken** (Durchschnitt, Min/Max Wortanzahl)
- **Tag-Häufigkeiten** der meist verwendeten Tags
- **Lesezeit-Berechnungen** (200 Wörter pro Minute)
### 🔧 Technische Verbesserungen
- **UI Helper Functions** in `utils/ui_helpers.py` für wiederverwendbare Komponenten
- **Verbesserte URL-Validierung** und Domain-Erkennung
- **Smart Content Selectors** für verschiedene Website-Typen
- **Robustes Error Handling** mit spezifischen Fehlermeldungen
- **Performance-Optimierungen** durch reduzierte `st.rerun()` Calls
- **Memory-Management** für große Artikel-Listen
### 📱 UX-Verbesserungen
- **Inline-Bearbeitung** von Artikel-Status direkt in der Card-Ansicht
- **Erweiterte Details-Ansicht** mit Collapsible-Bereichen
- **Copy-to-Clipboard** Funktionalität mit formatiertem Text
- **Hover-Effekte** und Animations für bessere Interaktion
- **Breadcrumb-Navigation** in komplexen Ansichten
- **Loading-Spinner** für längere Operationen
### 🗂️ Neue Dateistruktur
```
├── app.py (komplett überarbeitet)
├── main.py (erweiterte Backend-Logik)
├── utils/
│ ├── ui_helpers.py (neue UI-Komponenten)
│ ├── image_extractor.py (verbesserte Bildextraktion)
│ ├── article_extractor.py (erweiterte Artikelextraktion)
│ └── dalle_generator.py (unverändert)
├── pages/
│ ├── 01_feed_manager.py (bestehend)
│ └── log_viewer.py (bestehend)
└── logs/ (verbessertes Logging)
```
### 🔄 Migration & Kompatibilität
- **Vollständige Rückwärtskompatibilität** mit bestehenden JSON-Daten
- **Automatische Datenmigration** für neue Felder (source_name, word_count, etc.)
- **Graceful Degradation** bei fehlenden Feldern
- **Validierung und Reparatur** ungültiger Datenstrukturen
### ⚡ Performance-Optimierungen
- **Lazy Loading** für große Artikel-Listen
- **Effiziente Filtering** ohne komplette Neuladung
- **Optimierte Bildverarbeitung** mit Größen-Caching
- **Reduzierte API-Calls** durch besseres State Management
- **Memory-optimierte JSON-Verarbeitung**
### 🐛 Bugfixes
- **Status-Änderungen** gehen nicht mehr verloren nach Reload
- **Bildmetadaten** werden korrekt gespeichert und angezeigt
- **Duplikat-Artikel** werden zuverlässig erkannt
- **Encoding-Probleme** bei internationalen Zeichen behoben
- **Feed-Namen** werden korrekt in Filter-Dropdown angezeigt
- **Session State** Konflikte bei mehreren Tabs behoben
### 📋 Breaking Changes
- **Alte Tabellen-UI** wurde durch Card-Layout ersetzt
- **Sidebar-Navigation** wurde durch Tab-Navigation ersetzt
- **Direkte JSON-Manipulation** sollte vermieden werden (neue Validierung)
---
## [v1.5.3] - 2025-07-11 ## [v1.5.3] - 2025-07-11
### ✨ Neue Funktionen ### ✨ Neue Funktionen

View file

@ -1 +1 @@
VERSION = "1.5.3" VERSION = "1.6.2"

1004
app.py

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

20
internal/git.sh Normal file
View file

@ -0,0 +1,20 @@
# Aktuellen Stand vom main/master holen
git checkout main
git pull origin main
# Neuen Feature-Branch erstellen
git checkout -b feature/neue-funktion
# Entwickeln und committen
git add .
git commit -m "Neue Funktion implementiert"
# Branch auf Remote-Repository pushen
git push -u origin feature/neue-funktion
# Alle Branches anzeigen
git branch -a
# Aktuellen Branch anzeigen
git branch --show-current

View file

@ -218,3 +218,585 @@
2025-07-11 08:54:54,834 - INFO - 5 neue Artikel gespeichert. 2025-07-11 08:54:54,834 - INFO - 5 neue Artikel gespeichert.
2025-07-11 09:34:42,951 - INFO - ❌ Feed gelöscht: Promobil Ratgeber (https://www.promobil.de/rss/ratgeber) 2025-07-11 09:34:42,951 - INFO - ❌ Feed gelöscht: Promobil Ratgeber (https://www.promobil.de/rss/ratgeber)
2025-07-11 09:35:05,863 - INFO - 🔗 Neuer Feed hinzugefügt: Promobil Ratgeber (https://www.promobil.de/rss/ratgeber) 2025-07-11 09:35:05,863 - INFO - 🔗 Neuer Feed hinzugefügt: Promobil Ratgeber (https://www.promobil.de/rss/ratgeber)
2025-07-28 09:17:09,355 - INFO - ✍️ Umschreiben von: Preisschock bei Wohnmobilversicherungen: Versicherung gestiegen? Das können Sie tun!
2025-07-28 09:17:19,759 - INFO - Retrying request to /chat/completions in 0.484478 seconds
2025-07-28 09:17:30,386 - INFO - Retrying request to /chat/completions in 0.765465 seconds
2025-07-28 09:17:41,238 - ERROR - ❌ Fehler beim Umschreiben von 'Preisschock bei Wohnmobilversicherungen: Versicherung gestiegen? Das können Sie tun!':
Traceback (most recent call last):
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpx/_transports/default.py", line 101, in map_httpcore_exceptions
yield
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpx/_transports/default.py", line 250, in handle_request
resp = self._pool.handle_request(req)
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpcore/_sync/connection_pool.py", line 256, in handle_request
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpcore/_sync/connection_pool.py", line 236, in handle_request
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpcore/_sync/connection.py", line 101, in handle_request
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpcore/_sync/connection.py", line 78, in handle_request
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpcore/_sync/connection.py", line 124, in _connect
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpcore/_backends/sync.py", line 207, in connect_tcp
File "/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/contextlib.py", line 162, in __exit__
self.gen.throw(value)
~~~~~~~~~~~~~~^^^^^^^
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpcore/_exceptions.py", line 14, in map_exceptions
httpcore.ConnectError: [Errno 8] nodename nor servname provided, or not known
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/openai/_base_client.py", line 972, in request
response = self._client.send(
request,
stream=stream or self._should_stream_response_body(request=request),
**kwargs,
)
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpx/_client.py", line 914, in send
response = self._send_handling_auth(
request,
...<2 lines>...
history=[],
)
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpx/_client.py", line 942, in _send_handling_auth
response = self._send_handling_redirects(
request,
follow_redirects=follow_redirects,
history=history,
)
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpx/_client.py", line 979, in _send_handling_redirects
response = self._send_single_request(request)
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpx/_client.py", line 1014, in _send_single_request
response = transport.handle_request(request)
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpx/_transports/default.py", line 249, in handle_request
with map_httpcore_exceptions():
~~~~~~~~~~~~~~~~~~~~~~~^^
File "/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/contextlib.py", line 162, in __exit__
self.gen.throw(value)
~~~~~~~~~~~~~~^^^^^^^
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpx/_transports/default.py", line 118, in map_httpcore_exceptions
raise mapped_exc(message) from exc
httpx.ConnectError: [Errno 8] nodename nor servname provided, or not known
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/Users/oliver/Documents/rss-news/main.py", line 145, in rewrite_articles
response = openai.chat.completions.create(
model="gpt-4",
...<3 lines>...
]
)
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/openai/_utils/_utils.py", line 287, in wrapper
return func(*args, **kwargs)
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/openai/resources/chat/completions/completions.py", line 1087, in create
return self._post(
~~~~~~~~~~^
"/chat/completions",
^^^^^^^^^^^^^^^^^^^^
...<43 lines>...
stream_cls=Stream[ChatCompletionChunk],
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/openai/_base_client.py", line 1249, in post
return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/openai/_base_client.py", line 1004, in request
raise APIConnectionError(request=request) from err
openai.APIConnectionError: Connection error.
2025-07-28 09:18:02,091 - INFO - ✍️ Umschreiben von: Camper-Radio Caravan.fm : Radiosender speziell für Camping-Fans
2025-07-28 09:18:02,094 - INFO - Retrying request to /chat/completions in 0.415304 seconds
2025-07-28 09:18:02,517 - INFO - Retrying request to /chat/completions in 0.899018 seconds
2025-07-28 09:18:03,419 - ERROR - ❌ Fehler beim Umschreiben von 'Camper-Radio Caravan.fm : Radiosender speziell für Camping-Fans':
Traceback (most recent call last):
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpx/_transports/default.py", line 101, in map_httpcore_exceptions
yield
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpx/_transports/default.py", line 250, in handle_request
resp = self._pool.handle_request(req)
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpcore/_sync/connection_pool.py", line 256, in handle_request
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpcore/_sync/connection_pool.py", line 236, in handle_request
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpcore/_sync/connection.py", line 101, in handle_request
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpcore/_sync/connection.py", line 78, in handle_request
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpcore/_sync/connection.py", line 124, in _connect
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpcore/_backends/sync.py", line 207, in connect_tcp
File "/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/contextlib.py", line 162, in __exit__
self.gen.throw(value)
~~~~~~~~~~~~~~^^^^^^^
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpcore/_exceptions.py", line 14, in map_exceptions
httpcore.ConnectError: [Errno 8] nodename nor servname provided, or not known
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/openai/_base_client.py", line 972, in request
response = self._client.send(
request,
stream=stream or self._should_stream_response_body(request=request),
**kwargs,
)
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpx/_client.py", line 914, in send
response = self._send_handling_auth(
request,
...<2 lines>...
history=[],
)
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpx/_client.py", line 942, in _send_handling_auth
response = self._send_handling_redirects(
request,
follow_redirects=follow_redirects,
history=history,
)
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpx/_client.py", line 979, in _send_handling_redirects
response = self._send_single_request(request)
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpx/_client.py", line 1014, in _send_single_request
response = transport.handle_request(request)
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpx/_transports/default.py", line 249, in handle_request
with map_httpcore_exceptions():
~~~~~~~~~~~~~~~~~~~~~~~^^
File "/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/contextlib.py", line 162, in __exit__
self.gen.throw(value)
~~~~~~~~~~~~~~^^^^^^^
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/httpx/_transports/default.py", line 118, in map_httpcore_exceptions
raise mapped_exc(message) from exc
httpx.ConnectError: [Errno 8] nodename nor servname provided, or not known
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/Users/oliver/Documents/rss-news/main.py", line 145, in rewrite_articles
response = openai.chat.completions.create(
model="gpt-4",
...<3 lines>...
]
)
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/openai/_utils/_utils.py", line 287, in wrapper
return func(*args, **kwargs)
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/openai/resources/chat/completions/completions.py", line 1087, in create
return self._post(
~~~~~~~~~~^
"/chat/completions",
^^^^^^^^^^^^^^^^^^^^
...<43 lines>...
stream_cls=Stream[ChatCompletionChunk],
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/openai/_base_client.py", line 1249, in post
return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/oliver/Documents/rss-news/.venv/lib/python3.13/site-packages/openai/_base_client.py", line 1004, in request
raise APIConnectionError(request=request) from err
openai.APIConnectionError: Connection error.
2025-07-28 09:18:43,426 - INFO - ✍️ Umschreiben von: Preisschock bei Wohnmobilversicherungen: Versicherung gestiegen? Das können Sie tun!
2025-07-28 09:19:04,744 - INFO - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
2025-07-28 09:19:09,962 - INFO - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
2025-07-28 09:19:09,964 - INFO - ✅ Artikel umgeschrieben: Preisschock bei Wohnmobilversicherungen: Versicherung gestiegen? Das können Sie tun!
2025-07-28 09:19:09,964 - INFO - ✍️ Umschreiben von: Camper-Radio Caravan.fm : Radiosender speziell für Camping-Fans
2025-07-28 09:19:23,989 - INFO - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
2025-07-28 09:19:27,267 - INFO - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
2025-07-28 09:19:27,269 - INFO - ✅ Artikel umgeschrieben: Camper-Radio Caravan.fm : Radiosender speziell für Camping-Fans
2025-07-28 09:19:27,276 - INFO - Alle Artikel mit Status 'Rewrite' wurden verarbeitet.
2025-07-28 09:27:10,258 - INFO - 🧠 Generiere DALL·E-Bild für Prompt: Preisschock bei Wohnmobilversicherungen: Versicherung gestiegen? Das können Sie tun!
2025-07-28 09:27:26,502 - INFO - HTTP Request: POST https://api.openai.com/v1/images/generations "HTTP/1.1 200 OK"
2025-07-28 09:27:26,514 - INFO - ✅ Bild generiert: https://oaidalleapiprodscus.blob.core.windows.net/private/org-YimPc01cYtOXjUpCATUqDABw/user-eA31w0vmy3fOrb3G64Ygndsr/img-yKWdGDCQJZBOCQ4V4HoD40A0.png?st=2025-07-28T06%3A27%3A26Z&se=2025-07-28T08%3A27%3A26Z&sp=r&sv=2024-08-04&sr=b&rscd=inline&rsct=image/png&skoid=cc612491-d948-4d2e-9821-2683df3719f5&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2025-07-27T22%3A07%3A39Z&ske=2025-07-28T22%3A07%3A39Z&sks=b&skv=2024-08-04&sig=HUMRhg2FbaKnLil%2BMbyvNemVeBcrvTODpctkfQyFHPc%3D
2025-07-28 09:39:35,087 - INFO - Lade Feed: https://www.camping-news.de/rss/
2025-07-28 09:39:35,473 - INFO - 0 neue Artikel gefunden in https://www.camping-news.de/rss/
2025-07-28 09:39:35,473 - INFO - Lade Feed: https://www.promobil.de/rss/news
2025-07-28 09:39:35,914 - INFO - 0 neue Artikel gefunden in https://www.promobil.de/rss/news
2025-07-28 09:39:35,915 - INFO - Lade Feed: https://www.promobil.de/rss/ratgeber
2025-07-28 09:39:36,365 - INFO - 📷 Extrahiere Bilder von https://www.promobil.de/weitere-ratgeber/stunt-auf-wohnwagen-jensen-ackles-countdown/
2025-07-28 09:39:36,584 - INFO - 16 Bilder gefunden bei https://www.promobil.de/weitere-ratgeber/stunt-auf-wohnwagen-jensen-ackles-countdown/
2025-07-28 09:39:36,585 - INFO - 📷 Extrahiere Bilder von https://www.promobil.de/weitere-ratgeber/clever-campen-podcast-wie-passen-gravelbikes-und-camping-zusamen/
2025-07-28 09:39:36,793 - INFO - 13 Bilder gefunden bei https://www.promobil.de/weitere-ratgeber/clever-campen-podcast-wie-passen-gravelbikes-und-camping-zusamen/
2025-07-28 09:39:36,794 - INFO - 📷 Extrahiere Bilder von https://www.promobil.de/weitere-ratgeber/stellplatz-radar-30-tage-kostenlos-alle-plus-funktionen/
2025-07-28 09:39:36,999 - INFO - 15 Bilder gefunden bei https://www.promobil.de/weitere-ratgeber/stellplatz-radar-30-tage-kostenlos-alle-plus-funktionen/
2025-07-28 09:39:36,999 - INFO - 📷 Extrahiere Bilder von https://www.promobil.de/weitere-ratgeber/wann-der-digitale-fahrzeugschein-fuer-alle-kommt/
2025-07-28 09:39:37,219 - INFO - 14 Bilder gefunden bei https://www.promobil.de/weitere-ratgeber/wann-der-digitale-fahrzeugschein-fuer-alle-kommt/
2025-07-28 09:39:37,220 - INFO - 📷 Extrahiere Bilder von https://www.promobil.de/weitere-ratgeber/umfrage-welches-bad-brauchen-sie-im-wohnmobil/
2025-07-28 09:39:37,439 - INFO - 22 Bilder gefunden bei https://www.promobil.de/weitere-ratgeber/umfrage-welches-bad-brauchen-sie-im-wohnmobil/
2025-07-28 09:39:37,440 - INFO - 📷 Extrahiere Bilder von https://www.promobil.de/weitere-ratgeber/umfrage-kaffeegenuss-camping-wohnmobil-wohnwagen/
2025-07-28 09:39:37,744 - INFO - 13 Bilder gefunden bei https://www.promobil.de/weitere-ratgeber/umfrage-kaffeegenuss-camping-wohnmobil-wohnwagen/
2025-07-28 09:39:37,746 - INFO - 📷 Extrahiere Bilder von https://www.promobil.de/weitere-ratgeber/meinung-privates-mietbad-beim-camping-komfort-oder-stilbruch/
2025-07-28 09:39:37,983 - INFO - 17 Bilder gefunden bei https://www.promobil.de/weitere-ratgeber/meinung-privates-mietbad-beim-camping-komfort-oder-stilbruch/
2025-07-28 09:39:37,984 - INFO - 📷 Extrahiere Bilder von https://www.promobil.de/tipps/caravan-salon-duesseldorf-eine-einmalige-gelegenheit-fuer-camping-zubehoer-shopper/
2025-07-28 09:39:38,242 - INFO - 20 Bilder gefunden bei https://www.promobil.de/tipps/caravan-salon-duesseldorf-eine-einmalige-gelegenheit-fuer-camping-zubehoer-shopper/
2025-07-28 09:39:38,244 - INFO - 📷 Extrahiere Bilder von https://www.promobil.de/tipps/caravan-salon-2025-6-gruende-besuch-messe/
2025-07-28 09:39:38,476 - INFO - 20 Bilder gefunden bei https://www.promobil.de/tipps/caravan-salon-2025-6-gruende-besuch-messe/
2025-07-28 09:39:38,479 - INFO - 📷 Extrahiere Bilder von https://www.promobil.de/tipps/tipps-schutz-gegen-sommerhitze-hitzestau-wohnmobil/
2025-07-28 09:39:38,758 - INFO - 24 Bilder gefunden bei https://www.promobil.de/tipps/tipps-schutz-gegen-sommerhitze-hitzestau-wohnmobil/
2025-07-28 09:39:38,759 - INFO - 📷 Extrahiere Bilder von https://www.promobil.de/ratgeber/dethleffs-reiselust-praemie-2025-rabatte-wohnmobile/
2025-07-28 09:39:39,019 - INFO - 16 Bilder gefunden bei https://www.promobil.de/ratgeber/dethleffs-reiselust-praemie-2025-rabatte-wohnmobile/
2025-07-28 09:39:39,021 - INFO - 📷 Extrahiere Bilder von https://www.promobil.de/weitere-ratgeber/promobil-newsletter-ab-sofort-zwei-mal-die-woche-samstags-keine-camping-news-verpassen/
2025-07-28 09:39:39,254 - INFO - 13 Bilder gefunden bei https://www.promobil.de/weitere-ratgeber/promobil-newsletter-ab-sofort-zwei-mal-die-woche-samstags-keine-camping-news-verpassen/
2025-07-28 09:39:39,256 - INFO - 📷 Extrahiere Bilder von https://www.promobil.de/weitere-ratgeber/campingtourismus-boomt-drittes-camping-rekordjahr-in-folge/
2025-07-28 09:39:39,516 - INFO - 18 Bilder gefunden bei https://www.promobil.de/weitere-ratgeber/campingtourismus-boomt-drittes-camping-rekordjahr-in-folge/
2025-07-28 09:39:39,517 - INFO - 📷 Extrahiere Bilder von https://www.promobil.de/ratgeber/camping-ausruester-herzog-beantragt-insolvenz-wohnmobilhandel-belastet-vorzelthersteller/
2025-07-28 09:39:39,729 - INFO - 13 Bilder gefunden bei https://www.promobil.de/ratgeber/camping-ausruester-herzog-beantragt-insolvenz-wohnmobilhandel-belastet-vorzelthersteller/
2025-07-28 09:39:39,731 - INFO - 📷 Extrahiere Bilder von https://www.promobil.de/tipps/bordtechnik-einmaleins-einsteiger-tipps-fuer-den-campingurlaub/
2025-07-28 09:39:39,951 - INFO - 13 Bilder gefunden bei https://www.promobil.de/tipps/bordtechnik-einmaleins-einsteiger-tipps-fuer-den-campingurlaub/
2025-07-28 09:39:39,952 - INFO - 📷 Extrahiere Bilder von https://www.promobil.de/weitere-ratgeber/gotthard-brenner-reschenpass-wo-wohnmobile-und-camper-heute-geduld-brauchen/
2025-07-28 09:39:40,197 - INFO - 13 Bilder gefunden bei https://www.promobil.de/weitere-ratgeber/gotthard-brenner-reschenpass-wo-wohnmobile-und-camper-heute-geduld-brauchen/
2025-07-28 09:39:40,199 - INFO - 📷 Extrahiere Bilder von https://www.promobil.de/tipps/leichtbautricks-wohnmobilhersteller/
2025-07-28 09:39:40,458 - INFO - 13 Bilder gefunden bei https://www.promobil.de/tipps/leichtbautricks-wohnmobilhersteller/
2025-07-28 09:39:40,462 - INFO - 📷 Extrahiere Bilder von https://www.promobil.de/weitere-ratgeber/slowenische-campingmarken-sind-laengst-auf-dem-deutschen-markt-angekommen/
2025-07-28 09:39:40,695 - INFO - 16 Bilder gefunden bei https://www.promobil.de/weitere-ratgeber/slowenische-campingmarken-sind-laengst-auf-dem-deutschen-markt-angekommen/
2025-07-28 09:39:40,697 - INFO - 📷 Extrahiere Bilder von https://www.promobil.de/weitere-ratgeber/recap-folge-2-bella-italia-camping-auf-deutsch/
2025-07-28 09:39:40,919 - INFO - 14 Bilder gefunden bei https://www.promobil.de/weitere-ratgeber/recap-folge-2-bella-italia-camping-auf-deutsch/
2025-07-28 09:39:40,922 - INFO - 📷 Extrahiere Bilder von https://www.promobil.de/weitere-ratgeber/diese-kostenlosen-apps-muessen-camper-kennen/
2025-07-28 09:39:41,210 - INFO - 17 Bilder gefunden bei https://www.promobil.de/weitere-ratgeber/diese-kostenlosen-apps-muessen-camper-kennen/
2025-07-28 09:39:41,210 - INFO - 20 neue Artikel gefunden in https://www.promobil.de/rss/ratgeber
2025-07-28 09:39:41,211 - INFO - Lade Feed: https://caravan.fm/
2025-07-28 09:39:44,233 - INFO - 0 neue Artikel gefunden in https://caravan.fm/
2025-07-28 09:39:44,238 - INFO - 20 neue Artikel gespeichert.
2025-07-28 09:42:36,590 - INFO - ❌ Feed gelöscht: Neuer Feed (https://caravan.fm/)
2025-07-28 09:44:53,801 - INFO - ✍️ Umschreiben von: Pannen und Probleme im Wohnmobil & Wohnwagen: Erste Hilfe für die Camper-Bordtechnik
2025-07-28 09:45:18,500 - INFO - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
2025-07-28 09:45:21,113 - INFO - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
2025-07-28 09:45:21,126 - INFO - ✅ Artikel umgeschrieben: Pannen und Probleme im Wohnmobil & Wohnwagen: Erste Hilfe für die Camper-Bordtechnik
2025-07-28 09:45:21,146 - INFO - Alle Artikel mit Status 'Rewrite' wurden verarbeitet.
2025-07-28 10:29:47,016 - INFO - Lade Feed: https://www.camping-news.de/rss/
2025-07-28 10:29:47,407 - INFO - 0 neue Artikel gefunden in https://www.camping-news.de/rss/
2025-07-28 10:29:47,407 - INFO - Lade Feed: https://www.promobil.de/rss/news
2025-07-28 10:29:47,719 - INFO - 0 neue Artikel gefunden in https://www.promobil.de/rss/news
2025-07-28 10:29:47,719 - INFO - Lade Feed: https://www.promobil.de/rss/ratgeber
2025-07-28 10:29:48,183 - INFO - 0 neue Artikel gefunden in https://www.promobil.de/rss/ratgeber
2025-07-28 10:29:48,183 - INFO - Keine neuen Artikel gefunden.
2025-07-29 19:30:44,481 - INFO - Lade Feed: https://www.camping-news.de/rss/
2025-07-29 19:30:44,923 - INFO - 0 neue Artikel gefunden in https://www.camping-news.de/rss/
2025-07-29 19:30:44,923 - INFO - Lade Feed: https://www.promobil.de/rss/news
2025-07-29 19:30:45,348 - INFO - 0 neue Artikel gefunden in https://www.promobil.de/rss/news
2025-07-29 19:30:45,348 - INFO - Lade Feed: https://www.promobil.de/rss/ratgeber
2025-07-29 19:30:45,899 - INFO - 0 neue Artikel gefunden in https://www.promobil.de/rss/ratgeber
2025-07-29 19:30:45,899 - INFO - Keine neuen Artikel gefunden.
2025-08-15 09:44:18,677 - INFO - Lade Feed: https://www.camping-news.de/rss/
2025-08-15 09:44:18,993 - INFO - 0 neue Artikel gefunden in https://www.camping-news.de/rss/
2025-08-15 09:44:18,993 - INFO - Lade Feed: https://www.promobil.de/rss/news
2025-08-15 09:44:19,241 - INFO - 0 neue Artikel gefunden in https://www.promobil.de/rss/news
2025-08-15 09:44:19,241 - INFO - Lade Feed: https://www.promobil.de/rss/ratgeber
2025-08-15 09:44:19,550 - INFO - 🖼️ Starte Bildextraktion von: https://www.promobil.de/weitere-ratgeber/frankreichs-autobahnen-kein-adac-schutz-bei-pannen/
2025-08-15 09:44:19,709 - INFO - 🔍 12 img-Tags gefunden
2025-08-15 09:44:19,710 - INFO - ✅ Bild hinzugefügt: Bild aus Originalartikel...
2025-08-15 09:44:19,710 - ERROR - ❌ Unerwarteter Fehler bei Bildextraktion von https://www.promobil.de/weitere-ratgeber/frankreichs-autobahnen-kein-adac-schutz-bei-pannen/: unsupported operand type(s) for *: 'NoneType' and 'NoneType'
2025-08-15 09:44:19,710 - INFO - 🖼️ Starte Bildextraktion von: https://www.promobil.de/weitere-ratgeber/warntafel-wahnsinn-in-italien-anbringen-an-fahrradtraegern-trotz-neuer-gesetze-empfohlen/
2025-08-15 09:44:19,856 - INFO - 🔍 13 img-Tags gefunden
2025-08-15 09:44:19,856 - INFO - ✅ Bild hinzugefügt: 02/2024, Fahrradträger mit Warntafel...
2025-08-15 09:44:19,856 - INFO - ✅ Bild hinzugefügt: Bild aus Originalartikel...
2025-08-15 09:44:19,857 - ERROR - ❌ Unerwarteter Fehler bei Bildextraktion von https://www.promobil.de/weitere-ratgeber/warntafel-wahnsinn-in-italien-anbringen-an-fahrradtraegern-trotz-neuer-gesetze-empfohlen/: unsupported operand type(s) for *: 'NoneType' and 'NoneType'
2025-08-15 09:44:19,859 - INFO - 🖼️ Starte Bildextraktion von: https://www.promobil.de/tipps/achtung-mautschock-warum-viele-wohnmobile-bald-eine-go-box-brauchen/
2025-08-15 09:44:20,025 - INFO - 🔍 20 img-Tags gefunden
2025-08-15 09:44:20,025 - INFO - ✅ Bild hinzugefügt: Maut, Basis, Wissen, Österreich, Vignette, Go-Box,...
2025-08-15 09:44:20,025 - INFO - ✅ Bild hinzugefügt: Wohnmobil, Küste, Parkplatz, Wohnmobil, Mann...
2025-08-15 09:44:20,026 - INFO - ✅ Bild hinzugefügt: 05/2025, Spanien Polizei Verkehr...
2025-08-15 09:44:20,026 - INFO - ✅ Bild hinzugefügt: f_Autohof, Restaurant, essen, vegetarisch...
2025-08-15 09:44:20,026 - INFO - ✅ Bild hinzugefügt: Supercheck, Dethleffs Magic Edition T 2 EB, Seiten...
2025-08-15 09:44:20,026 - INFO - 🎉 5 Bilder erfolgreich extrahiert von https://www.promobil.de/tipps/achtung-mautschock-warum-viele-wohnmobile-bald-eine-go-box-brauchen/
2025-08-15 09:44:20,026 - INFO - 3 neue Artikel gefunden in https://www.promobil.de/rss/ratgeber
2025-08-15 09:44:20,038 - INFO - 3 neue Artikel gespeichert.
2025-08-15 09:45:55,607 - INFO - ✍️ Umschreiben von: Fahrradtransport in Italien: Warntafel bei Fahrradträgern doch wieder Pflicht?
2025-08-15 09:46:09,158 - INFO - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
2025-08-15 09:46:11,508 - INFO - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
2025-08-15 09:46:11,564 - INFO - ✅ Artikel umgeschrieben: Fahrradtransport in Italien: Warntafel bei Fahrradträgern doch wieder Pflicht?
2025-08-15 09:46:11,565 - INFO - ✍️ Umschreiben von: Neue Mautregeln für Wohnmobile mit 3,5 t: Diese Camper brauchen ab dem Stichtag eine GO-Box
2025-08-15 09:46:32,092 - INFO - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
2025-08-15 09:46:34,549 - INFO - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
2025-08-15 09:46:34,552 - INFO - ✅ Artikel umgeschrieben: Neue Mautregeln für Wohnmobile mit 3,5 t: Diese Camper brauchen ab dem Stichtag eine GO-Box
2025-08-15 09:46:34,571 - INFO - Alle Artikel mit Status 'Rewrite' wurden verarbeitet.
2025-08-15 09:48:30,972 - INFO - 🧠 Generiere DALL·E-Bild für Prompt: Fahrradtransport in Italien: Warntafel bei Fahrradträgern doch wieder Pflicht?
2025-08-15 09:48:42,548 - INFO - HTTP Request: POST https://api.openai.com/v1/images/generations "HTTP/1.1 200 OK"
2025-08-15 09:48:42,559 - INFO - ✅ Bild generiert: https://oaidalleapiprodscus.blob.core.windows.net/private/org-YimPc01cYtOXjUpCATUqDABw/user-eA31w0vmy3fOrb3G64Ygndsr/img-Ksiks2ssSZxpEFf1MQedlap1.png?st=2025-08-15T06%3A48%3A42Z&se=2025-08-15T08%3A48%3A42Z&sp=r&sv=2024-08-04&sr=b&rscd=inline&rsct=image/png&skoid=8b33a531-2df9-46a3-bc02-d4b1430a422c&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2025-08-14T16%3A59%3A16Z&ske=2025-08-15T16%3A59%3A16Z&sks=b&skv=2024-08-04&sig=e0/ULpNgNLwixo3UapqnxHgR18t4HCpyEtnbmik33yA%3D
2025-08-15 09:51:43,090 - INFO - 🧠 Generiere DALL·E-Bild für Prompt: Neue Mautregeln für Wohnmobile mit 3,5 t: Diese Camper brauchen ab dem Stichtag eine GO-Box
2025-08-15 09:51:53,907 - INFO - HTTP Request: POST https://api.openai.com/v1/images/generations "HTTP/1.1 200 OK"
2025-08-15 09:51:53,914 - INFO - ✅ Bild generiert: https://oaidalleapiprodscus.blob.core.windows.net/private/org-YimPc01cYtOXjUpCATUqDABw/user-eA31w0vmy3fOrb3G64Ygndsr/img-Pl4ik6W7mTrv2MhIbdWlwgOL.png?st=2025-08-15T06%3A51%3A53Z&se=2025-08-15T08%3A51%3A53Z&sp=r&sv=2024-08-04&sr=b&rscd=inline&rsct=image/png&skoid=b1a0ae1f-618f-4548-84fd-8b16cacd5485&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2025-08-14T15%3A09%3A17Z&ske=2025-08-15T15%3A09%3A17Z&sks=b&skv=2024-08-04&sig=RHIFlJLMumrcr/jEskOVfqJ%2Bns0pDS2HM8l5siBfLmM%3D
2025-08-15 09:55:42,370 - INFO - Lade Feed: https://www.camping-news.de/rss/
2025-08-15 09:55:42,639 - INFO - 0 neue Artikel gefunden in https://www.camping-news.de/rss/
2025-08-15 09:55:42,640 - INFO - Lade Feed: https://www.promobil.de/rss/news
2025-08-15 09:55:42,843 - INFO - 0 neue Artikel gefunden in https://www.promobil.de/rss/news
2025-08-15 09:55:42,843 - INFO - Lade Feed: https://www.promobil.de/rss/ratgeber
2025-08-15 09:55:43,180 - INFO - 0 neue Artikel gefunden in https://www.promobil.de/rss/ratgeber
2025-08-15 09:55:43,180 - INFO - Keine neuen Artikel gefunden.
2025-08-16 12:11:35,892 - INFO - load_articles:123 - ✅ 53 Artikel geladen
2025-08-16 12:11:35,893 - INFO - load_feeds:92 - ✅ 3 Feeds geladen
2025-08-16 12:42:43,093 - INFO - load_articles:124 - ✅ 53 Artikel geladen
2025-08-16 12:42:43,095 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:48:41,965 - INFO - load_articles:124 - ✅ 53 Artikel geladen
2025-08-16 12:48:41,966 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:49:23,544 - INFO - load_articles:124 - ✅ 53 Artikel geladen
2025-08-16 12:49:23,544 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:49:23,547 - INFO - process_articles:268 - 🚀 Starte Artikel-Verarbeitung
2025-08-16 12:49:23,547 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:49:23,554 - INFO - load_articles:124 - ✅ 53 Artikel geladen
2025-08-16 12:49:23,554 - INFO - fetch_and_process_feed:174 - 🔄 Verarbeite Feed: https://www.camping-news.de/rss/
2025-08-16 12:49:25,076 - INFO - fetch_and_process_feed:181 - 📡 Feed-Name: Camping-News
2025-08-16 12:49:25,076 - INFO - fetch_and_process_feed:187 - 📰 10 Einträge gefunden
2025-08-16 12:49:25,079 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: 15 Jahre PremiumCamps
2025-08-16 12:49:25,080 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Der neue Eriba Feeling und Novaline
2025-08-16 12:49:25,081 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Den eigenen Verbrauchszahlen auf der Spur
2025-08-16 12:49:25,083 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Abenteuer für die Kleinen, Entspannung für
die Großen
2025-08-16 12:49:25,084 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Das weltweit größte Caravaning-Erlebnis
2025-08-16 12:49:25,086 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Komfort und Flexibilität für moderne Camper
2025-08-16 12:49:25,087 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Frühjahrsaktionen vom Verein WOHNmobil für Klimaschutz
2025-08-16 12:49:25,090 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Viel los auf dem Klaukenhof
2025-08-16 12:49:25,092 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Camping Resort Allweglehen bietet "Wellness plus"
2025-08-16 12:49:25,093 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: EU-Führerscheinreform kommt
2025-08-16 12:49:25,093 - INFO - fetch_and_process_feed:257 - ✅ Feed verarbeitet: 0 neue Artikel aus https://www.camping-news.de/rss/
2025-08-16 12:49:26,098 - INFO - fetch_and_process_feed:174 - 🔄 Verarbeite Feed: https://www.promobil.de/rss/news
2025-08-16 12:49:26,570 - INFO - fetch_and_process_feed:181 - 📡 Feed-Name: News bei www.promobil.de
2025-08-16 12:49:26,571 - INFO - fetch_and_process_feed:187 - 📰 20 Einträge gefunden
2025-08-16 12:49:26,571 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Vanlife-Events im Juni bis November 2025: Events für Bulli-Lover und Campervan-Fans
2025-08-16 12:49:26,571 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/termine-veranstaltungen-juni-juli/ (Versuch 1)
2025-08-16 12:49:27,928 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren
2025-08-16 12:49:27,930 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None}
2025-08-16 12:49:27,931 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 649 Wörter
2025-08-16 12:49:27,931 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 649 Wörter
2025-08-16 12:49:27,931 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Vanlife-Events im Juni bis November 2025: Events für Bulli-Lover und Campervan-Fans
2025-08-16 12:49:27,932 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Schutz vor Einbruch und Diebstahl fürs Wohnmobil: Wie schütze ich mein Wohnmobil vor Diebstahl?
2025-08-16 12:49:27,932 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/sicherheitszubehoer-wohnmobil-schutz-einbruch-diebstahl-gase/ (Versuch 1)
2025-08-16 12:49:28,227 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren
2025-08-16 12:49:28,228 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None}
2025-08-16 12:49:28,230 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 1134 Wörter
2025-08-16 12:49:28,230 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 1134 Wörter
2025-08-16 12:49:28,230 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Schutz vor Einbruch und Diebstahl fürs Wohnmobil: Wie schütze ich mein Wohnmobil vor Diebstahl?
2025-08-16 12:49:28,231 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Hymer kündigt neue Wohnmobil-Marke Corigon an: Hymer bringt neue Günstig-Marke für Camper
2025-08-16 12:49:28,231 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/corigon-neue-hymer-marke-sommer-2025/ (Versuch 1)
2025-08-16 12:49:28,591 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren
2025-08-16 12:49:28,593 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None}
2025-08-16 12:49:28,594 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 874 Wörter
2025-08-16 12:49:28,594 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 874 Wörter
2025-08-16 12:49:28,594 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Hymer kündigt neue Wohnmobil-Marke Corigon an: Hymer bringt neue Günstig-Marke für Camper
2025-08-16 12:49:28,594 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Die besten Camper-Innovationen 2025: „Frosch“ ist das genialste neue Camping-Zubehör
2025-08-16 12:49:28,594 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/sieger-frosch-campfire-der-ideen-2025/ (Versuch 1)
2025-08-16 12:49:30,472 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren
2025-08-16 12:49:30,473 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None}
2025-08-16 12:49:30,473 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 452 Wörter
2025-08-16 12:49:30,474 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 452 Wörter
2025-08-16 12:49:30,474 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Die besten Camper-Innovationen 2025: „Frosch“ ist das genialste neue Camping-Zubehör
2025-08-16 12:49:30,474 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Leserstimmen zum Update der 9-Gang-Automatik: Was bringt das Softwareupdate für den Ducato 8?
2025-08-16 12:49:30,475 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/fiat-ducato-9-gang-automatik-softwareupdate/ (Versuch 1)
2025-08-16 12:49:31,156 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren
2025-08-16 12:49:31,157 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None}
2025-08-16 12:49:31,160 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 1546 Wörter
2025-08-16 12:49:31,160 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 1546 Wörter
2025-08-16 12:49:31,161 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Leserstimmen zum Update der 9-Gang-Automatik: Was bringt das Softwareupdate für den Ducato 8?
2025-08-16 12:49:31,161 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: CARAVANING Jahrgangs Archiv 2024: CARAVANING fürs Archiv
2025-08-16 12:49:31,161 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/caravaning-jahrgangs-archiv-als-pdf-download/ (Versuch 1)
2025-08-16 12:49:32,094 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren
2025-08-16 12:49:32,095 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None}
2025-08-16 12:49:32,095 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 136 Wörter
2025-08-16 12:49:32,096 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 136 Wörter
2025-08-16 12:49:32,096 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: CARAVANING Jahrgangs Archiv 2024: CARAVANING fürs Archiv
2025-08-16 12:49:32,096 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Top 10 Clever-Campen-Videos 2024: Die beliebtesten Camping-Videos
2025-08-16 12:49:32,096 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/top-10-clever-campen-videos-2024/ (Versuch 1)
2025-08-16 12:49:33,747 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren
2025-08-16 12:49:33,748 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None}
2025-08-16 12:49:33,749 - INFO - extract_full_article:262 - 🔄 Fallback auf Paragraph-Extraktion
2025-08-16 12:49:33,749 - INFO - extract_from_paragraphs:194 - ✅ Fallback-Extraktion aus 18 Paragraphen
2025-08-16 12:49:33,749 - INFO - extract_full_article:271 - 🔄 Letzter Fallback: Body-Text
2025-08-16 12:49:33,752 - INFO - extract_full_article:281 - ⚠️ Body-Extraktion: 0 Wörter
2025-08-16 12:49:33,752 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Top 10 Clever-Campen-Videos 2024: Die beliebtesten Camping-Videos
2025-08-16 12:49:33,752 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Retro-Campingfahrzeuge aus den Nuller-Jahren: Das waren die heißesten Wohnmobile 2004
2025-08-16 12:49:33,752 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/retro-wohnmobile-2004-nuller-jahre-rueckblick-promobil/ (Versuch 1)
2025-08-16 12:49:34,562 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren
2025-08-16 12:49:34,564 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None}
2025-08-16 12:49:34,566 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 1870 Wörter
2025-08-16 12:49:34,567 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 1870 Wörter
2025-08-16 12:49:34,567 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Retro-Campingfahrzeuge aus den Nuller-Jahren: Das waren die heißesten Wohnmobile 2004
2025-08-16 12:49:34,567 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Gaswarner im Wohnmobil: So schütze Sie sich vor unsichtbaren Gasen
2025-08-16 12:49:34,568 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/gaswarner-wohnmobil-schutz-gas/ (Versuch 1)
2025-08-16 12:49:35,350 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren
2025-08-16 12:49:35,352 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None}
2025-08-16 12:49:35,354 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 1796 Wörter
2025-08-16 12:49:35,354 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 1796 Wörter
2025-08-16 12:49:35,355 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Gaswarner im Wohnmobil: So schütze Sie sich vor unsichtbaren Gasen
2025-08-16 12:49:35,355 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Preisschock bei Wohnmobilversicherungen: Versicherung gestiegen? Das können Sie tun!
2025-08-16 12:49:35,355 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/versicherungen-werden-2025-teuerer-tipps-und-tricks-um-die-kosten-zu-senken/ (Versuch 1)
2025-08-16 12:49:35,626 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren
2025-08-16 12:49:35,627 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None}
2025-08-16 12:49:35,628 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 942 Wörter
2025-08-16 12:49:35,629 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 942 Wörter
2025-08-16 12:49:35,629 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Preisschock bei Wohnmobilversicherungen: Versicherung gestiegen? Das können Sie tun!
2025-08-16 12:49:35,629 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Dailycamper Trekking (2025): Neuer Mini-Camper auf Fiat Doblo
2025-08-16 12:49:35,629 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/dailycamper-trekking-mini-camper-fiat-doblo/ (Versuch 1)
2025-08-16 12:49:35,899 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren
2025-08-16 12:49:35,900 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None}
2025-08-16 12:49:35,901 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 416 Wörter
2025-08-16 12:49:35,901 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 416 Wörter
2025-08-16 12:49:35,901 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Dailycamper Trekking (2025): Neuer Mini-Camper auf Fiat Doblo
2025-08-16 12:49:35,901 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Campingbus Rocket Camper Ryzon beim Weltrekord: Jonas Deichmanns Camper für Ironman-Rekord
2025-08-16 12:49:35,902 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: E-Trailer für die Campingfahrzeug-Steuerung per App: So werden Wohnwagen & Camper zum Smarthome
2025-08-16 12:49:35,902 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/e-trailer-system-kontrollboard-wohnwagen-camper/ (Versuch 1)
2025-08-16 12:49:36,613 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren
2025-08-16 12:49:36,614 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None}
2025-08-16 12:49:36,614 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 499 Wörter
2025-08-16 12:49:36,615 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 499 Wörter
2025-08-16 12:49:36,615 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: E-Trailer für die Campingfahrzeug-Steuerung per App: So werden Wohnwagen & Camper zum Smarthome
2025-08-16 12:49:36,615 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Im Wohnmobil zum 2024-EM-Spielort Leipzig: Nach Leipzig in die Red Bull Arena zur Fußball-EM
2025-08-16 12:49:36,615 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/spielorte-em-2024-red-bull-arena-leipzig/ (Versuch 1)
2025-08-16 12:49:38,736 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren
2025-08-16 12:49:38,737 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None}
2025-08-16 12:49:38,739 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 894 Wörter
2025-08-16 12:49:38,739 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 894 Wörter
2025-08-16 12:49:38,739 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Im Wohnmobil zum 2024-EM-Spielort Leipzig: Nach Leipzig in die Red Bull Arena zur Fußball-EM
2025-08-16 12:49:38,740 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Can-Kasim Dogan von Westfalen Mobil im Interview : Wie sieht der Westfalia-CEO die Camping-Zukunft?
2025-08-16 12:49:38,740 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/can-kasim-dogan-interview-westfalia/ (Versuch 1)
2025-08-16 12:49:39,605 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren
2025-08-16 12:49:39,606 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None}
2025-08-16 12:49:39,607 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 599 Wörter
2025-08-16 12:49:39,607 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 599 Wörter
2025-08-16 12:49:39,607 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Can-Kasim Dogan von Westfalen Mobil im Interview : Wie sieht der Westfalia-CEO die Camping-Zukunft?
2025-08-16 12:49:39,608 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Top-Fortbewegungsmittel für Wohnmobil-Reisende: Wie bleiben Sie im Campingurlaub mobil?
2025-08-16 12:49:39,608 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/promobil-leser-meinung-mobilitaet-fortbewegungsmittel-fahrrad/ (Versuch 1)
2025-08-16 12:49:40,376 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren
2025-08-16 12:49:40,377 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None}
2025-08-16 12:49:40,378 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 639 Wörter
2025-08-16 12:49:40,378 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 639 Wörter
2025-08-16 12:49:40,378 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Top-Fortbewegungsmittel für Wohnmobil-Reisende: Wie bleiben Sie im Campingurlaub mobil?
2025-08-16 12:49:40,379 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Camping-Tipps für EM-Spielorte 2024 : Frankfurt für Fußball-Fans im Wohnmobil
2025-08-16 12:49:40,379 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/spielorte-em-2024-frankfurt/ (Versuch 1)
2025-08-16 12:49:41,173 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren
2025-08-16 12:49:41,174 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None}
2025-08-16 12:49:41,175 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 691 Wörter
2025-08-16 12:49:41,176 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 691 Wörter
2025-08-16 12:49:41,176 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Camping-Tipps für EM-Spielorte 2024 : Frankfurt für Fußball-Fans im Wohnmobil
2025-08-16 12:49:41,176 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Spielorte und Stellplätze zur Fußball-EM 2024 : Mit dem Wohnmobil zur Euro 2024 in Stuttgart
2025-08-16 12:49:41,176 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/stuttgart-em-2024-fussball-stellplaetze-wohnmobil-camping/ (Versuch 1)
2025-08-16 12:49:42,081 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren
2025-08-16 12:49:42,083 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None}
2025-08-16 12:49:42,084 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 923 Wörter
2025-08-16 12:49:42,085 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 923 Wörter
2025-08-16 12:49:42,085 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Spielorte und Stellplätze zur Fußball-EM 2024 : Mit dem Wohnmobil zur Euro 2024 in Stuttgart
2025-08-16 12:49:42,085 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Camper-Radio Caravan.fm : Radiosender speziell für Camping-Fans
2025-08-16 12:49:42,085 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/camper-radio-radiosender-caravan-fm/ (Versuch 1)
2025-08-16 12:49:42,899 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren
2025-08-16 12:49:42,900 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None}
2025-08-16 12:49:42,901 - INFO - extract_full_article:257 - 🎉 Erfolgreiche Extraktion (generisch): 258 Wörter
2025-08-16 12:49:42,901 - INFO - fetch_and_process_feed:215 - ✅ Volltext extrahiert: 258 Wörter
2025-08-16 12:49:42,901 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Camper-Radio Caravan.fm : Radiosender speziell für Camping-Fans
2025-08-16 12:49:42,902 - INFO - fetch_and_process_feed:211 - 🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: Professor für Tourismus-Management im Interview: Armin Brysch spricht über virtuelle Reisen
2025-08-16 12:49:42,902 - INFO - extract_full_article:214 - 📰 Starte Volltextextraktion von: https://www.promobil.de/tourismus-management-armin-brysch-interview-virtuelle-reisen/ (Versuch 1)
2025-08-16 12:49:44,184 - INFO - extract_full_article:253 - 🔄 Fallback auf generische Selektoren
2025-08-16 12:49:44,185 - INFO - extract_with_selectors:165 - ✅ Erfolgreiche Extraktion mit Selektor: {'tag': 'article', 'class': None}
2025-08-16 12:49:44,186 - INFO - extract_full_article:262 - 🔄 Fallback auf Paragraph-Extraktion
2025-08-16 12:49:44,186 - INFO - extract_from_paragraphs:194 - ✅ Fallback-Extraktion aus 8 Paragraphen
2025-08-16 12:49:44,187 - INFO - extract_full_article:271 - 🔄 Letzter Fallback: Body-Text
2025-08-16 12:49:44,189 - INFO - extract_full_article:281 - ⚠️ Body-Extraktion: 0 Wörter
2025-08-16 12:49:44,190 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Professor für Tourismus-Management im Interview: Armin Brysch spricht über virtuelle Reisen
2025-08-16 12:49:44,190 - INFO - fetch_and_process_feed:257 - ✅ Feed verarbeitet: 0 neue Artikel aus https://www.promobil.de/rss/news
2025-08-16 12:49:45,196 - INFO - fetch_and_process_feed:174 - 🔄 Verarbeite Feed: https://www.promobil.de/rss/ratgeber
2025-08-16 12:49:46,821 - INFO - fetch_and_process_feed:181 - 📡 Feed-Name: ratgeber bei www.promobil.de
2025-08-16 12:49:46,821 - INFO - fetch_and_process_feed:187 - 📰 20 Einträge gefunden
2025-08-16 12:49:46,822 - INFO - extract_images_with_metadata:149 - 🖼️ Starte Bildextraktion von: https://www.promobil.de/weitere-ratgeber/gasflaschenservice-fuer-camping-und-grillfans-im-promobil-test/
2025-08-16 12:49:47,648 - INFO - extract_images_with_metadata:167 - 🔍 16 img-Tags gefunden
2025-08-16 12:49:47,649 - INFO - extract_images_with_metadata:180 - ✅ Bild hinzugefügt: Automat...
2025-08-16 12:49:47,649 - INFO - extract_images_with_metadata:180 - ✅ Bild hinzugefügt: Camping, Gas...
2025-08-16 12:49:47,650 - INFO - extract_images_with_metadata:180 - ✅ Bild hinzugefügt: Gasflaschen, Tauschautomat, Energie Rath, Dinkels...
2025-08-16 12:49:47,650 - INFO - extract_images_with_metadata:180 - ✅ Bild hinzugefügt: LPG...
2025-08-16 12:49:47,650 - INFO - extract_images_with_metadata:180 - ✅ Bild hinzugefügt: Bild aus Originalartikel...
2025-08-16 12:49:47,650 - ERROR - extract_images_with_metadata:200 - ❌ Unerwarteter Fehler bei Bildextraktion von https://www.promobil.de/weitere-ratgeber/gasflaschenservice-fuer-camping-und-grillfans-im-promobil-test/: unsupported operand type(s) for *: 'NoneType' and 'NoneType'
2025-08-16 12:49:47,651 - INFO - fetch_and_process_feed:244 - 🖼️ 0 Bilder für 'Tauschautomat für Gasflaschen im Praxischeck: Wie funktioniert der 24/7-Service für Camper' extrahiert
2025-08-16 12:49:47,651 - INFO - fetch_and_process_feed:249 - ✅ Neuer Artikel hinzugefügt: Tauschautomat für Gasflaschen im Praxischeck: Wie funktioniert der 24/7-Service für Camper
2025-08-16 12:49:47,652 - INFO - extract_images_with_metadata:149 - 🖼️ Starte Bildextraktion von: https://www.promobil.de/weitere-ratgeber/neue-bussgelder-in-italien-falsche-muellentsorgung-aus-dem-wohnmobil-wird-besonders-teuer/
2025-08-16 12:49:50,073 - INFO - extract_images_with_metadata:167 - 🔍 13 img-Tags gefunden
2025-08-16 12:49:50,074 - INFO - extract_images_with_metadata:180 - ✅ Bild hinzugefügt: Basiswissen, Der gute Ton, Stellplatz-Knigge, Müll...
2025-08-16 12:49:50,074 - INFO - extract_images_with_metadata:180 - ✅ Bild hinzugefügt: Bild aus Originalartikel...
2025-08-16 12:49:50,074 - ERROR - extract_images_with_metadata:200 - ❌ Unerwarteter Fehler bei Bildextraktion von https://www.promobil.de/weitere-ratgeber/neue-bussgelder-in-italien-falsche-muellentsorgung-aus-dem-wohnmobil-wird-besonders-teuer/: unsupported operand type(s) for *: 'NoneType' and 'NoneType'
2025-08-16 12:49:50,075 - INFO - fetch_and_process_feed:244 - 🖼️ 0 Bilder für 'Italien verschärft Regeln zur Müllentsorgung: Bis zu 18.000 Euro Strafe für Müll vor dem Camper' extrahiert
2025-08-16 12:49:50,075 - INFO - fetch_and_process_feed:249 - ✅ Neuer Artikel hinzugefügt: Italien verschärft Regeln zur Müllentsorgung: Bis zu 18.000 Euro Strafe für Müll vor dem Camper
2025-08-16 12:49:50,076 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Regeln auf Frankreichs Autobahnen im Pannenfall: Kein Schutz durch ADAC & Co. auf Autobahnen
2025-08-16 12:49:50,078 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Fahrradtransport in Italien: Warntafel bei Fahrradträgern doch wieder Pflicht?
2025-08-16 12:49:50,081 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Neue Mautregeln für Wohnmobile mit 3,5 t: Diese Camper brauchen ab dem Stichtag eine GO-Box
2025-08-16 12:49:50,083 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Film-Stunt mit Caravan wie realistisch ist das?: Sexsymbol Jensen Ackles wagt Stunt auf Wohnwagen
2025-08-16 12:49:50,085 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: CLEVER CAMPEN Podcast Folge 40: Gravelbikes und Camping die beste Kombi?
2025-08-16 12:49:50,086 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Stellplatz-Radar Sommeraktion 2025: 30 Tage Stellplatz-Radar PLUS gratis testen
2025-08-16 12:49:50,088 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Digitaler Fahrzeugschein für Camper: promobil testet den digitalen Fahrzeugschein
2025-08-16 12:49:50,089 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Badezimmer beim Camping - Umfrage: Welches Bad brauchen Sie im Wohnmobil?
2025-08-16 12:49:50,091 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Umfrage zum Kaffeegenuss beim Camping: So kochen Sie am liebsten Ihren Kaffee
2025-08-16 12:49:50,092 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Zwei Menschen, zwei Meinungen: Das Mietbad spaltet die Campingwelt
2025-08-16 12:49:50,094 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Zubehör auf der größten Campingmesse kaufen: Deshalb müssen Zubehör-Shopper zum Caravan Salon
2025-08-16 12:49:50,096 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Tipps für die größte Camping-Messe Deutschlands: Darum dürfen Sie den Caravan Salon nicht verpassen
2025-08-16 12:49:50,099 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Sommerhitze im Wohnmobil & Wohnwagen: Die besten Tipps gegen Hitze im Camper
2025-08-16 12:49:50,101 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Dethleffs Reiselust-Prämie: Bis zu 20.000 Euro Rabatt auf Wohnmobile
2025-08-16 12:49:50,102 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Der neue promobil-Newsletter - gratis!: Zum Frühstück die spannendsten Camping-Themen
2025-08-16 12:49:50,103 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Drittes Camping-Rekordjahr in Folge: Süd schlägt Nord Hier wird am häufigsten gecampt
2025-08-16 12:49:50,104 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Wohnmobil-Handel treibt Vorzelt-Profi in Insolvenz: Camping-Ausrüster Herzog beantragt Insolvenz
2025-08-16 12:49:50,106 - INFO - fetch_and_process_feed:251 - 🔄 Duplikat übersprungen: Pannen und Probleme im Wohnmobil & Wohnwagen: Erste Hilfe für die Camper-Bordtechnik
2025-08-16 12:49:50,106 - INFO - fetch_and_process_feed:257 - ✅ Feed verarbeitet: 2 neue Artikel aus https://www.promobil.de/rss/ratgeber
2025-08-16 12:49:51,131 - INFO - save_articles:144 - ✅ 55 Artikel gespeichert
2025-08-16 12:49:51,131 - INFO - process_articles:310 - 🎉 Verarbeitung abgeschlossen: 2 neue Artikel in 27.58s hinzugefügt
2025-08-16 12:49:52,222 - INFO - load_articles:124 - ✅ 55 Artikel geladen
2025-08-16 12:49:52,223 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:50:27,304 - INFO - load_articles:124 - ✅ 55 Artikel geladen
2025-08-16 12:50:27,305 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:50:27,334 - INFO - save_articles:144 - ✅ 55 Artikel gespeichert
2025-08-16 12:50:27,909 - INFO - load_articles:124 - ✅ 55 Artikel geladen
2025-08-16 12:50:27,910 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:50:33,666 - INFO - load_articles:124 - ✅ 55 Artikel geladen
2025-08-16 12:50:33,667 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:50:47,623 - INFO - load_articles:124 - ✅ 55 Artikel geladen
2025-08-16 12:50:47,624 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:50:49,242 - INFO - _get_default_category_id:63 - ✅ Standard-Kategorie 'Allgemein' gefunden: ID 1
2025-08-16 12:50:49,242 - INFO - test_connection:237 - 🔧 Teste WordPress-API-Verbindung...
2025-08-16 12:50:49,751 - INFO - test_connection:247 - ✅ WordPress-API-Verbindung erfolgreich
2025-08-16 12:51:22,341 - INFO - load_articles:124 - ✅ 55 Artikel geladen
2025-08-16 12:51:22,342 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:51:22,344 - INFO - rewrite_articles:320 - ✍️ Starte Artikel-Umschreibung
2025-08-16 12:51:22,346 - INFO - load_articles:124 - ✅ 55 Artikel geladen
2025-08-16 12:51:22,347 - INFO - rewrite_articles:337 - ✍️ Umschreiben von: Tauschautomat für Gasflaschen im Praxischeck: Wie funktioniert der 24/7-Service für Camper
2025-08-16 12:51:37,237 - INFO - _send_single_request:1025 - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
2025-08-16 12:51:39,037 - INFO - _send_single_request:1025 - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
2025-08-16 12:51:39,038 - INFO - rewrite_articles:392 - ✅ Artikel erfolgreich umgeschrieben: Tauschautomat für Gasflaschen im Praxischeck: Wie funktioniert der 24/7-Service für Camper
2025-08-16 12:51:41,055 - INFO - save_articles:144 - ✅ 55 Artikel gespeichert
2025-08-16 12:51:41,055 - INFO - rewrite_articles:404 - 🎉 1 Artikel erfolgreich umgeschrieben
2025-08-16 12:51:42,128 - INFO - load_articles:124 - ✅ 55 Artikel geladen
2025-08-16 12:51:42,128 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:52:01,541 - INFO - load_articles:124 - ✅ 55 Artikel geladen
2025-08-16 12:52:01,542 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:52:08,641 - INFO - load_articles:124 - ✅ 55 Artikel geladen
2025-08-16 12:52:08,641 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:52:14,550 - INFO - load_articles:124 - ✅ 55 Artikel geladen
2025-08-16 12:52:14,550 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:52:32,308 - INFO - load_articles:124 - ✅ 55 Artikel geladen
2025-08-16 12:52:32,311 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:53:12,402 - INFO - load_articles:124 - ✅ 55 Artikel geladen
2025-08-16 12:53:12,403 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:53:15,939 - INFO - load_articles:124 - ✅ 55 Artikel geladen
2025-08-16 12:53:15,939 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:53:15,959 - INFO - save_articles:144 - ✅ 55 Artikel gespeichert
2025-08-16 12:53:16,006 - INFO - load_articles:124 - ✅ 55 Artikel geladen
2025-08-16 12:53:16,006 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:53:21,156 - INFO - load_articles:124 - ✅ 55 Artikel geladen
2025-08-16 12:53:21,156 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:53:21,821 - INFO - _get_default_category_id:63 - ✅ Standard-Kategorie 'Allgemein' gefunden: ID 1
2025-08-16 12:53:21,821 - INFO - test_connection:237 - 🔧 Teste WordPress-API-Verbindung...
2025-08-16 12:53:22,205 - INFO - test_connection:247 - ✅ WordPress-API-Verbindung erfolgreich
2025-08-16 12:53:22,205 - INFO - upload_article:158 - 📤 Starte WordPress-Upload: Tauschautomat für Gasflaschen im Praxischeck: Wie funktioniert der 24/7-Service für Camper
2025-08-16 12:53:23,117 - ERROR - upload_article:190 - ❌ WordPress-Fehler 400 für 'Tauschautomat für Gasflaschen im Praxischeck: Wie funktioniert der 24/7-Service für Camper': Ungültige(r) Parameter: tags
2025-08-16 12:53:24,214 - INFO - load_articles:124 - ✅ 55 Artikel geladen
2025-08-16 12:53:24,214 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:58:02,027 - INFO - load_articles:124 - ✅ 55 Artikel geladen
2025-08-16 12:58:02,028 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:58:14,389 - INFO - load_articles:124 - ✅ 55 Artikel geladen
2025-08-16 12:58:14,391 - INFO - load_feeds:93 - ✅ 3 Feeds geladen
2025-08-16 12:58:14,395 - INFO - upload_articles_to_wp:412 - 📤 Starte WordPress-Upload
2025-08-16 12:58:14,408 - INFO - load_articles:124 - ✅ 55 Artikel geladen
2025-08-16 12:58:14,408 - INFO - upload_articles_to_wp:421 - 📦 1 Artikel für WordPress-Upload gefunden
2025-08-16 12:58:14,410 - INFO - __init__:51 - ✅ WordPress-Authentifizierung: Verwende bereitgestellten Base64-String
2025-08-16 12:58:16,007 - INFO - _get_default_category_id:87 - ✅ Standard-Kategorie 'Allgemein' gefunden: ID 1
2025-08-16 12:58:16,007 - INFO - test_connection:338 - 🔧 Teste WordPress-API-Verbindung mit Base64-Auth...
2025-08-16 12:58:16,007 - INFO - test_connection:342 - 🔑 Authorization Header: Basic b2dpZXJ0ejp3aE...
2025-08-16 12:58:16,501 - INFO - test_connection:351 - 📡 API-Response Status: 200
2025-08-16 12:58:16,501 - INFO - test_connection:352 - 📡 API-Response Headers: {'Content-Type': 'application/json; charset=UTF-8', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', 'X-WS-RateLimit-Limit': '1000', 'X-WS-RateLimit-Remaining': '998', 'Date': 'Sat, 16 Aug 2025 10:58:16 GMT', 'Server': 'Apache', 'X-Powered-By': 'PHP/8.2.29', 'Pragma': 'no-cache', 'X-Robots-Tag': 'noindex', 'X-Content-Type-Options': 'nosniff', 'Access-Control-Expose-Headers': 'X-WP-Total, X-WP-TotalPages, Link', 'Access-Control-Allow-Headers': 'Authorization, X-WP-Nonce, Content-Disposition, Content-MD5, Content-Type', 'X-WP-Total': '4', 'X-WP-TotalPages': '4', 'Link': '<https://vanityontour.de/wp-json/wp/v2/categories?per_page=1&page=2>; rel="next"', 'Allow': 'GET, POST', 'Expires': 'Wed, 11 Jan 1984 05:00:00 GMT', 'Cache-Control': 'no-cache, must-revalidate, max-age=0, no-store, private'}
2025-08-16 12:58:16,501 - INFO - test_connection:355 - ✅ WordPress-API-Verbindung erfolgreich
2025-08-16 12:58:16,501 - INFO - upload_multiple_articles:392 - 📦 Starte Batch-Upload von 1 Artikeln zu WordPress
2025-08-16 12:58:16,502 - INFO - upload_multiple_articles:396 - 📤 Upload 1/1: Tauschautomat für Gasflaschen im Praxischeck: Wie funktioniert der 24/7-Service für Camper
2025-08-16 12:58:16,502 - INFO - upload_article:249 - 📤 Starte WordPress-Upload: Tauschautomat für Gasflaschen im Praxischeck: Wie funktioniert der 24/7-Service für Camper
2025-08-16 12:58:18,037 - INFO - _get_or_create_tags:149 - ✅ Neuer Tag erstellt: 'Gasflaschen-Automat' (ID: 891)
2025-08-16 12:58:18,550 - INFO - _get_or_create_tags:135 - ✅ Existierender Tag gefunden: 'Wohnmobil' (ID: 646)
2025-08-16 12:58:19,573 - INFO - _get_or_create_tags:149 - ✅ Neuer Tag erstellt: 'Globus-Baumarkt' (ID: 892)
2025-08-16 12:58:19,573 - INFO - _get_or_create_tags:158 - 🏷️ Tags verarbeitet: 3 Tag-IDs erstellt
2025-08-16 12:58:19,573 - INFO - _prepare_post_data:194 - 📝 Post-Daten vorbereitet: Titel='Tauschautomat für Gasflaschen im Praxischeck: Wie funktioniert der 24/7-Service für Camper', Tags=3, Kategorie=1
2025-08-16 12:58:20,442 - INFO - upload_article:274 - ✅ WordPress-Upload erfolgreich: 'Tauschautomat für Gasflaschen im Praxischeck: Wie funktioniert der 24/7-Service für Camper' (ID: 3378)
2025-08-16 12:58:20,442 - INFO - upload_article:275 - 🔗 WordPress-URL: https://vanityontour.de/?p=3378
2025-08-16 12:58:20,442 - INFO - upload_multiple_articles:422 - 📊 Batch-Upload abgeschlossen: 1 erfolgreich, 0 fehlgeschlagen, 0 Duplikate
2025-08-16 12:58:20,443 - INFO - upload_articles_to_wp:441 - ✅ Status geändert für 'Tauschautomat für Gasflaschen im Praxischeck: Wie funktioniert der 24/7-Service für Camper': Process → WordPress Pending
2025-08-16 12:58:20,453 - INFO - save_articles:144 - ✅ 55 Artikel gespeichert
2025-08-16 12:58:20,454 - INFO - upload_articles_to_wp:446 - 💾 Artikel-Status nach WordPress-Upload aktualisiert
2025-08-16 12:58:22,520 - INFO - load_articles:124 - ✅ 55 Artikel geladen
2025-08-16 12:58:22,522 - INFO - load_feeds:93 - ✅ 3 Feeds geladen

463
main.py
View file

@ -10,6 +10,9 @@ import logging
import openai import openai
from utils.image_extractor import extract_images_with_metadata from utils.image_extractor import extract_images_with_metadata
from utils.article_extractor import extract_full_article from utils.article_extractor import extract_full_article
from utils.wordpress_uploader import upload_articles_to_wordpress
import hashlib
import time
load_dotenv() load_dotenv()
@ -17,168 +20,474 @@ load_dotenv()
log_dir = "logs" log_dir = "logs"
os.makedirs(log_dir, exist_ok=True) os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, "rss_tool.log") log_file = os.path.join(log_dir, "rss_tool.log")
# Logging-Format verbessern
logging.basicConfig( logging.basicConfig(
filename=log_file,
level=logging.INFO, level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s" format="%(asctime)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s",
handlers=[
logging.FileHandler(log_file, encoding='utf-8'),
logging.StreamHandler() # Auch in Konsole ausgeben
]
) )
openai.api_key = os.getenv("OPENAI_API_KEY") openai.api_key = os.getenv("OPENAI_API_KEY")
ARTICLES_FILE = "data/articles.json" ARTICLES_FILE = "data/articles.json"
FEEDS_FILE = "data/feeds.json" FEEDS_FILE = "data/feeds.json"
VALID_STATUSES = ["New", "Rewrite", "Process", "Online", "On Hold", "Trash"] VALID_STATUSES = ["New", "Rewrite", "Process", "Online", "On Hold", "Trash", "WordPress Pending"]
# === Datenordner erstellen ===
os.makedirs("data", exist_ok=True)
def generate_article_id(title, link, date):
"""Generiert eine eindeutige ID für einen Artikel basierend auf mehreren Attributen"""
identifier = f"{title}_{link}_{date}"
return hashlib.md5(identifier.encode('utf-8')).hexdigest()
def is_duplicate_article(new_article, existing_articles):
"""Prüft ob ein Artikel bereits existiert (erweiterte Duplikatserkennung)"""
new_title = new_article.get("title", "").lower().strip()
new_link = new_article.get("link", "").strip()
for existing in existing_articles:
existing_title = existing.get("title", "").lower().strip()
existing_link = existing.get("link", "").strip()
# Exakte URL-Übereinstimmung
if new_link and existing_link and new_link == existing_link:
return True
# Sehr ähnliche Titel (mindestens 90% Übereinstimmung)
if new_title and existing_title:
similarity = calculate_similarity(new_title, existing_title)
if similarity > 0.9:
return True
return False
def calculate_similarity(text1, text2):
"""Berechnet die Ähnlichkeit zwischen zwei Texten (vereinfachte Methode)"""
words1 = set(text1.split())
words2 = set(text2.split())
if not words1 and not words2:
return 1.0
if not words1 or not words2:
return 0.0
intersection = len(words1.intersection(words2))
union = len(words1.union(words2))
return intersection / union if union > 0 else 0.0
def load_feeds(): def load_feeds():
"""Lädt RSS-Feeds aus der JSON-Datei"""
try:
if not os.path.exists(FEEDS_FILE): if not os.path.exists(FEEDS_FILE):
logging.info("Feeds-Datei existiert nicht, erstelle leere Liste")
return [] return []
with open(FEEDS_FILE, "r") as f:
return json.load(f)
with open(FEEDS_FILE, "r", encoding='utf-8') as f:
feeds = json.load(f)
logging.info(f"{len(feeds)} Feeds geladen")
return feeds
except Exception as e:
logging.error(f"❌ Fehler beim Laden der Feeds: {e}")
return []
def save_feeds(feeds): def save_feeds(feeds):
with open(FEEDS_FILE, "w") as f: """Speichert RSS-Feeds in die JSON-Datei"""
json.dump(feeds, f, indent=2) try:
with open(FEEDS_FILE, "w", encoding='utf-8') as f:
json.dump(feeds, f, indent=2, ensure_ascii=False)
logging.info(f"{len(feeds)} Feeds gespeichert")
except Exception as e:
logging.error(f"❌ Fehler beim Speichern der Feeds: {e}")
def load_articles(): def load_articles():
"""Lädt Artikel aus der JSON-Datei"""
try:
if not os.path.exists(ARTICLES_FILE): if not os.path.exists(ARTICLES_FILE):
logging.info("Artikel-Datei existiert nicht, erstelle leere Liste")
return [] return []
with open(ARTICLES_FILE, "r") as f:
with open(ARTICLES_FILE, "r", encoding='utf-8') as f:
articles = json.load(f) articles = json.load(f)
# Status-Validierung
for article in articles: for article in articles:
if article.get("status") not in VALID_STATUSES: if article.get("status") not in VALID_STATUSES:
article["status"] = "New" article["status"] = "New"
return articles logging.warning(f"⚠️ Ungültiger Status für Artikel '{article.get('title', 'Unbekannt')}' korrigiert")
logging.info(f"{len(articles)} Artikel geladen")
return articles
except Exception as e:
logging.error(f"❌ Fehler beim Laden der Artikel: {e}")
return []
def save_articles(articles): def save_articles(articles):
with open(ARTICLES_FILE, "w") as f: """Speichert Artikel in die JSON-Datei"""
json.dump(articles, f, indent=2) try:
# Validierung vor dem Speichern
valid_articles = []
for article in articles:
if "id" in article and "title" in article:
valid_articles.append(article)
else:
logging.warning(f"⚠️ Ungültiger Artikel übersprungen: {article}")
with open(ARTICLES_FILE, "w", encoding='utf-8') as f:
json.dump(valid_articles, f, indent=2, ensure_ascii=False)
def fetch_and_process_feed(feed_url, existing_ids): logging.info(f"{len(valid_articles)} Artikel gespeichert")
feed = feedparser.parse(feed_url) except Exception as e:
new_articles = [] logging.error(f"❌ Fehler beim Speichern der Artikel: {e}")
for entry in feed.entries:
article_id = entry.get("id") or entry.get("link")
if not article_id or article_id in existing_ids:
continue
title = entry.get("title", "Kein Titel")
date = entry.get("published", datetime.now().isoformat())
summary = entry.get("summary", "")
content = entry.get("content", [{}])[0].get("value") or entry.get("description", "")
def clean_html_content(content):
"""Bereinigt HTML-Inhalt und extrahiert Text"""
try:
soup = BeautifulSoup(content, "html.parser") soup = BeautifulSoup(content, "html.parser")
# Entferne Script- und Style-Tags
for script in soup(["script", "style"]):
script.decompose()
# Hole sauberen Text
clean_text = soup.get_text(" ", strip=True) clean_text = soup.get_text(" ", strip=True)
# Automatischer Volltext-Fetch bei zu wenig Wörtern # Entferne überschüssige Leerzeichen
if len(clean_text.split()) < 50 and entry.get("link"): clean_text = " ".join(clean_text.split())
fetched_text = extract_full_article(entry["link"])
return clean_text
except Exception as e:
logging.error(f"❌ Fehler beim Bereinigen des HTML-Inhalts: {e}")
return content
def fetch_and_process_feed(feed_url, existing_articles):
"""Lädt und verarbeitet einen einzelnen RSS-Feed"""
new_articles = []
feed_name = "Unbekannt"
try:
logging.info(f"🔄 Verarbeite Feed: {feed_url}")
# Feed parsen
feed = feedparser.parse(feed_url)
if hasattr(feed, 'feed') and hasattr(feed.feed, 'title'):
feed_name = feed.feed.title
logging.info(f"📡 Feed-Name: {feed_name}")
if not feed.entries:
logging.warning(f"⚠️ Keine Einträge in Feed gefunden: {feed_url}")
return []
logging.info(f"📰 {len(feed.entries)} Einträge gefunden")
for entry in feed.entries:
try:
# Basis-Informationen extrahieren
title = entry.get("title", "Kein Titel")
date = entry.get("published", datetime.now().isoformat())
link = entry.get("link", "")
summary = entry.get("summary", "")
# Content extrahieren
content = ""
if hasattr(entry, 'content') and entry.content:
content = entry.content[0].get("value", "")
elif hasattr(entry, 'description'):
content = entry.description
else:
content = summary
# HTML bereinigen
clean_text = clean_html_content(content)
# Volltext-Extraktion bei kurzen Artikeln
if len(clean_text.split()) < 50 and link:
logging.info(f"🔍 Kurzer Artikel erkannt, versuche Volltext-Extraktion: {title}")
fetched_text = extract_full_article(link)
if len(fetched_text.split()) > len(clean_text.split()): if len(fetched_text.split()) > len(clean_text.split()):
clean_text = fetched_text clean_text = fetched_text
logging.info(f"✅ Volltext extrahiert: {len(clean_text.split())} Wörter")
images = extract_images_with_metadata(entry.link) # Artikel-ID generieren
article_id = generate_article_id(title, link, date)
new_articles.append({ # Neuen Artikel erstellen
new_article = {
"id": article_id, "id": article_id,
"title": title, "title": title,
"date": date, "date": date,
"summary": summary, "summary": summary[:300] + "..." if len(summary) > 300 else summary,
"text": clean_text, "text": clean_text,
"tags": [], "tags": [],
"status": "New", "status": "New",
"link": entry.get("link", ""), "link": link,
"images": images, "images": [],
"source": feed_url "source": feed_url,
}) "source_name": feed_name,
"created_at": datetime.now().isoformat(),
"word_count": len(clean_text.split())
}
# Duplikatsprüfung
if not is_duplicate_article(new_article, existing_articles):
# Bilder extrahieren
if link:
try:
images = extract_images_with_metadata(link)
new_article["images"] = images
logging.info(f"🖼️ {len(images)} Bilder für '{title}' extrahiert")
except Exception as e:
logging.error(f"❌ Fehler bei Bildextraktion für '{title}': {e}")
new_articles.append(new_article)
logging.info(f"✅ Neuer Artikel hinzugefügt: {title}")
else:
logging.info(f"🔄 Duplikat übersprungen: {title}")
except Exception as e:
logging.error(f"❌ Fehler beim Verarbeiten des Eintrags '{entry.get('title', 'Unbekannt')}': {e}")
continue
logging.info(f"✅ Feed verarbeitet: {len(new_articles)} neue Artikel aus {feed_url}")
return new_articles return new_articles
except Exception as e:
logging.error(f"❌ Kritischer Fehler beim Verarbeiten von {feed_url}: {e}")
return []
def process_articles(existing_ids=None):
"""Verarbeitet alle RSS-Feeds und fügt neue Artikel hinzu"""
try:
start_time = time.time()
logging.info("🚀 Starte Artikel-Verarbeitung")
def process_articles(existing_ids):
feeds = load_feeds() feeds = load_feeds()
all_articles = load_articles() all_articles = load_articles()
articles_by_id = {article["id"]: article for article in all_articles if "id" in article}
new_entries = [] if not feeds:
logging.warning("⚠️ Keine RSS-Feeds konfiguriert")
return
# Bestehende Artikel für Duplikatsprüfung
existing_articles = all_articles.copy()
total_new_articles = 0
for feed in feeds: for feed in feeds:
url = feed.get("url") if isinstance(feed, dict) else feed feed_url = feed.get("url") if isinstance(feed, dict) else feed
if not url:
if not feed_url:
logging.warning("⚠️ Feed ohne URL übersprungen")
continue continue
try: try:
logging.info(f"Lade Feed: {url}") new_articles = fetch_and_process_feed(feed_url, existing_articles)
entries = fetch_and_process_feed(url, existing_ids)
new_entries.extend(entries) # Neue Artikel zur Gesamtliste hinzufügen
logging.info(f"{len(entries)} neue Artikel gefunden in {url}") for article in new_articles:
all_articles.append(article)
existing_articles.append(article) # Für weitere Duplikatsprüfung
total_new_articles += len(new_articles)
# Kurze Pause zwischen Feeds
time.sleep(1)
except Exception as e: except Exception as e:
logging.exception(f"Fehler beim Verarbeiten von {url}:") logging.error(f"❌ Fehler beim Verarbeiten von Feed {feed_url}: {e}")
continue
added = 0 # Artikel speichern
for entry in new_entries: if total_new_articles > 0:
if entry["id"] not in articles_by_id: save_articles(all_articles)
articles_by_id[entry["id"]] = entry processing_time = time.time() - start_time
added += 1 logging.info(f"🎉 Verarbeitung abgeschlossen: {total_new_articles} neue Artikel in {processing_time:.2f}s hinzugefügt")
else: else:
logging.info(f"Artikel bereits vorhanden, wird übersprungen: {entry['title']}") logging.info(" Keine neuen Artikel gefunden")
if added > 0:
save_articles(list(articles_by_id.values()))
logging.info(f"{added} neue Artikel gespeichert.")
else:
logging.info("Keine neuen Artikel gefunden.")
except Exception as e:
logging.error(f"❌ Kritischer Fehler bei der Artikel-Verarbeitung: {e}")
def rewrite_articles(): def rewrite_articles():
"""Schreibt Artikel mit Status 'Rewrite' um"""
try:
logging.info("✍️ Starte Artikel-Umschreibung")
articles = load_articles() articles = load_articles()
rewrite_articles_list = [a for a in articles if a.get("status") == "Rewrite"]
if not rewrite_articles_list:
logging.info(" Keine Artikel zum Umschreiben gefunden")
return
if not openai.api_key:
logging.error("❌ OpenAI API-Key nicht konfiguriert")
return
changed = False changed = False
for article in articles: for article in rewrite_articles_list:
if article.get("status") == "Rewrite":
try: try:
logging.info(f"✍️ Umschreiben von: {article['title']}") logging.info(f"✍️ Umschreiben von: {article['title']}")
prompt = f"Schreibe folgenden Artikel um und fasse ihn verständlich zusammen:\n\n{article['text']}"
# Artikel umschreiben
prompt = f"""Schreibe den folgenden Artikel um und fasse ihn verständlich zusammen.
Behalte die wichtigsten Informationen bei, aber formuliere alles neu:
{article['text']}"""
response = openai.chat.completions.create( response = openai.chat.completions.create(
model="gpt-4", model="gpt-4",
messages=[ messages=[
{"role": "system", "content": "Du bist ein professioneller Redakteur."}, {"role": "system", "content": "Du bist ein professioneller Redakteur, der Artikel umschreibt und verbessert."},
{"role": "user", "content": prompt} {"role": "user", "content": prompt}
] ],
max_tokens=1500,
temperature=0.7
) )
new_text = response.choices[0].message.content.strip()
article["text"] = f"{article['title']}\n\n{new_text}"
article["status"] = "Process"
tag_prompt = f"Erstelle 3 passende, kurze Stichwörter (Tags) für diesen Artikel:\n\n{new_text}" new_text = response.choices[0].message.content.strip()
# Tags generieren
tag_prompt = f"""Erstelle 3-5 passende, kurze Stichwörter (Tags) für diesen Artikel.
Gib nur die Tags zurück, getrennt durch Kommas:
{new_text}"""
tag_response = openai.chat.completions.create( tag_response = openai.chat.completions.create(
model="gpt-4", model="gpt-4",
messages=[ messages=[
{"role": "system", "content": "Du bist ein Blog-Tag-Generator."}, {"role": "system", "content": "Du generierst präzise Tags für Blog-Artikel."},
{"role": "user", "content": tag_prompt} {"role": "user", "content": tag_prompt}
] ],
max_tokens=100,
temperature=0.5
) )
tags_raw = tag_response.choices[0].message.content.strip()
tags = [tag.strip(" ,") for tag in tags_raw.replace("\n", ",").split(",") if tag.strip()]
article["tags"] = tags
tags_raw = tag_response.choices[0].message.content.strip()
tags = [tag.strip().strip(',') for tag in tags_raw.split(",") if tag.strip()]
# Artikel aktualisieren
article["text"] = new_text
article["tags"] = tags
article["status"] = "Process"
article["rewritten_at"] = datetime.now().isoformat()
article["word_count"] = len(new_text.split())
# Bildmetadaten vervollständigen falls nötig
for img in article.get("images", []): for img in article.get("images", []):
if "caption" not in img: if "caption" not in img or not img["caption"]:
img["caption"] = "Kein Bildtitel vorhanden" img["caption"] = "Kein Bildtitel vorhanden"
if "copyright" not in img: if "copyright" not in img or not img["copyright"]:
img["copyright"] = "Unbekannt" img["copyright"] = "Unbekannt"
if "copyright_url" not in img: if "copyright_url" not in img or not img["copyright_url"]:
img["copyright_url"] = "#" img["copyright_url"] = "#"
logging.info(f"✅ Artikel umgeschrieben: {article['title']}") logging.info(f"✅ Artikel erfolgreich umgeschrieben: {article['title']}")
changed = True changed = True
# Kurze Pause zwischen API-Calls
time.sleep(2)
except Exception as e: except Exception as e:
logging.exception(f"❌ Fehler beim Umschreiben von '{article['title']}':") logging.error(f"❌ Fehler beim Umschreiben von '{article['title']}': {e}")
continue
if changed: if changed:
save_articles(articles) save_articles(articles)
logging.info("Alle Artikel mit Status 'Rewrite' wurden verarbeitet.") logging.info(f"🎉 {len(rewrite_articles_list)} Artikel erfolgreich umgeschrieben")
except Exception as e:
logging.error(f"❌ Kritischer Fehler beim Umschreiben: {e}")
def upload_articles_to_wp():
"""Lädt Artikel mit Status 'Process' zu WordPress hoch"""
try:
logging.info("📤 Starte WordPress-Upload")
articles = load_articles()
process_articles_list = [a for a in articles if a.get("status") == "Process"]
if not process_articles_list:
logging.info(" Keine Artikel für WordPress-Upload gefunden")
return {"total": 0, "successful": 0, "failed": 0, "message": "Keine Artikel zum Hochladen gefunden"}
logging.info(f"📦 {len(process_articles_list)} Artikel für WordPress-Upload gefunden")
# WordPress-Upload durchführen
upload_results = upload_articles_to_wordpress(process_articles_list)
# Status der erfolgreich hochgeladenen Artikel ändern
if upload_results.get('successful', 0) > 0:
changed = False
for detail in upload_results.get('details', []):
if detail.get('success'):
article_id = detail.get('article_id')
# Artikel in der Liste finden und Status ändern
for article in articles:
if article.get('id') == article_id:
article['status'] = "WordPress Pending"
article['wp_upload_date'] = datetime.now().isoformat()
article['wp_post_id'] = detail.get('wp_post_id')
changed = True
logging.info(f"✅ Status geändert für '{article.get('title')}': Process → WordPress Pending")
break
if changed:
save_articles(articles)
logging.info(f"💾 Artikel-Status nach WordPress-Upload aktualisiert")
return upload_results
except Exception as e:
logging.error(f"❌ Kritischer Fehler beim WordPress-Upload: {e}")
return {"total": 0, "successful": 0, "failed": 1, "error": str(e)}
def get_article_stats():
"""Gibt Statistiken über die Artikel zurück"""
try:
articles = load_articles()
stats = {
"total_articles": len(articles),
"status_distribution": {},
"word_count_stats": {},
"source_distribution": {},
"images_count": 0
}
# Status-Verteilung
for article in articles:
status = article.get("status", "New")
stats["status_distribution"][status] = stats["status_distribution"].get(status, 0) + 1
# Wortanzahl-Statistiken
word_counts = [article.get("word_count", 0) for article in articles if article.get("word_count")]
if word_counts:
stats["word_count_stats"] = {
"average": sum(word_counts) // len(word_counts),
"min": min(word_counts),
"max": max(word_counts)
}
# Quellen-Verteilung
for article in articles:
source = article.get("source_name", "Unbekannt")
stats["source_distribution"][source] = stats["source_distribution"].get(source, 0) + 1
# Bilder zählen
stats["images_count"] = sum(len(article.get("images", [])) for article in articles)
return stats
except Exception as e:
logging.error(f"❌ Fehler beim Erstellen der Statistiken: {e}")
return {}

View file

@ -2,26 +2,362 @@
import requests import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import logging
import time
from typing import Optional
def extract_full_article(url: str) -> str: # Konfiguration
REQUEST_TIMEOUT = 15
MAX_RETRIES = 3
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
# Website-spezifische Selektoren
CONTENT_SELECTORS = {
# Promobil & Camping-spezifisch
'promobil.de': [
{'tag': 'div', 'class': 'article__text'},
{'tag': 'div', 'class': 'article-content'},
{'tag': 'div', 'class': 'content-text'}
],
'camping.info': [
{'tag': 'div', 'class': 'article-body'},
{'tag': 'div', 'class': 'post-content'}
],
'caravaning.de': [
{'tag': 'div', 'class': 'article__content'},
{'tag': 'div', 'class': 'entry-content'}
],
# WordPress Standard-Selektoren
'wordpress': [
{'tag': 'div', 'class': 'entry-content'},
{'tag': 'div', 'class': 'post-content'},
{'tag': 'div', 'class': 'content'},
{'tag': 'main', 'class': 'main-content'},
{'tag': 'article', 'class': None}
],
# Allgemeine Fallbacks
'generic': [
{'tag': 'article', 'class': None},
{'tag': 'div', 'class': 'content'},
{'tag': 'div', 'class': 'post'},
{'tag': 'div', 'class': 'entry'},
{'tag': 'main', 'class': None},
{'tag': 'div', 'id': 'content'},
{'tag': 'div', 'id': 'main'}
]
}
def get_domain_from_url(url: str) -> str:
"""
Extrahiert die Domain aus einer URL
"""
try: try:
response = requests.get(url, timeout=10) from urllib.parse import urlparse
response.raise_for_status() parsed = urlparse(url)
soup = BeautifulSoup(response.text, "html.parser") return parsed.netloc.lower()
except:
return ""
# Promobil & WordPress & allgemeine Fallbacks def get_selectors_for_domain(domain: str) -> list:
candidates = [ """
{"tag": "div", "class_": "article__text"}, # Promobil Gibt die passenden Selektoren für eine Domain zurück
{"tag": "div", "class_": "entry-content"}, # WordPress Standard """
{"tag": "article", "class_": None}, # Generisch # Direkte Domain-Matches
for known_domain in CONTENT_SELECTORS:
if known_domain != 'wordpress' and known_domain != 'generic' and known_domain in domain:
return CONTENT_SELECTORS[known_domain]
# WordPress erkennen (wird später durch Meta-Tags erkannt)
return CONTENT_SELECTORS['generic']
def is_wordpress_site(soup: BeautifulSoup) -> bool:
"""
Erkennt WordPress-Websites anhand von Meta-Tags
"""
try:
# WordPress Generator Meta-Tag
generator = soup.find('meta', attrs={'name': 'generator'})
if generator and 'wordpress' in generator.get('content', '').lower():
return True
# WordPress-spezifische Link-Tags
wp_links = soup.find_all('link', href=lambda x: x and '/wp-' in x)
if wp_links:
return True
# WordPress REST API
rest_api = soup.find('link', attrs={'rel': 'https://api.w.org/'})
if rest_api:
return True
return False
except:
return False
def clean_extracted_text(text: str) -> str:
"""
Bereinigt extrahierten Text von unerwünschten Elementen
"""
if not text:
return ""
lines = text.split('\n')
cleaned_lines = []
for line in lines:
line = line.strip()
# Überspringe sehr kurze Zeilen (wahrscheinlich Navigation/Werbung)
if len(line) < 10:
continue
# Überspringe typische Navigation/Footer-Texte
skip_patterns = [
'cookie', 'datenschutz', 'impressum', 'agb', 'newsletter',
'folgen sie uns', 'social media', 'teilen', 'weiterlesen',
'mehr zum thema', 'ähnliche artikel', 'kommentare',
'anzeige', 'werbung', 'advertisement'
] ]
for selector in candidates: if any(pattern in line.lower() for pattern in skip_patterns):
el = soup.find(selector["tag"], class_=selector["class_"]) continue
if el and len(el.get_text(strip=True).split()) > 50:
return el.get_text(" ", strip=True) # Überspringe Zeilen mit zu vielen Sonderzeichen (Navigation)
if len([c for c in line if c in '|•→←↑↓']) > 3:
continue
cleaned_lines.append(line)
# Text zusammenfügen
cleaned_text = ' '.join(cleaned_lines)
# Mehrfache Leerzeichen entfernen
cleaned_text = ' '.join(cleaned_text.split())
return cleaned_text
def extract_with_selectors(soup: BeautifulSoup, selectors: list) -> str:
"""
Versucht Text mit einer Liste von Selektoren zu extrahieren
"""
for selector in selectors:
try:
element = None
if selector.get('class'):
element = soup.find(selector['tag'], class_=selector['class'])
elif selector.get('id'):
element = soup.find(selector['tag'], id=selector['id'])
else:
element = soup.find(selector['tag'])
if element:
# Entferne Script- und Style-Tags
for script in element(['script', 'style', 'nav', 'header', 'footer', 'aside']):
script.decompose()
text = element.get_text(' ', strip=True)
# Nur zurückgeben wenn genügend Text vorhanden
if len(text.split()) > 50:
logging.info(f"✅ Erfolgreiche Extraktion mit Selektor: {selector}")
return clean_extracted_text(text)
except Exception as e:
logging.debug(f"Selektor {selector} fehlgeschlagen: {e}")
continue
# Fallback: ganzer Seiteninhalt
return soup.get_text(" ", strip=True)
except Exception:
return "" return ""
def extract_from_paragraphs(soup: BeautifulSoup) -> str:
"""
Fallback: Extrahiert Text aus allen Paragraph-Tags
"""
try:
paragraphs = soup.find_all('p')
if not paragraphs:
return ""
# Sammle alle Paragraph-Texte
texts = []
for p in paragraphs:
text = p.get_text(strip=True)
if len(text) > 20: # Nur längere Absätze
texts.append(text)
combined_text = ' '.join(texts)
if len(combined_text.split()) > 30:
logging.info(f"✅ Fallback-Extraktion aus {len(paragraphs)} Paragraphen")
return clean_extracted_text(combined_text)
return ""
except Exception as e:
logging.error(f"Fehler bei Paragraph-Extraktion: {e}")
return ""
def extract_full_article(url: str) -> str:
"""
Hauptfunktion: Extrahiert den vollständigen Artikeltext von einer URL
"""
if not url:
return ""
retries = 0
while retries < MAX_RETRIES:
try:
logging.info(f"📰 Starte Volltextextraktion von: {url} (Versuch {retries + 1})")
# HTTP-Request mit verbessertem Header
headers = {
'User-Agent': USER_AGENT,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
}
response = requests.get(url, timeout=REQUEST_TIMEOUT, headers=headers)
response.raise_for_status()
# Encoding sicherstellen
if response.encoding.lower() in ['iso-8859-1', 'windows-1252']:
response.encoding = 'utf-8'
soup = BeautifulSoup(response.text, "html.parser")
# Domain-spezifische Selektoren ermitteln
domain = get_domain_from_url(url)
selectors = get_selectors_for_domain(domain)
# WordPress erkennen und entsprechende Selektoren verwenden
if is_wordpress_site(soup):
logging.info("🔧 WordPress-Site erkannt")
selectors = CONTENT_SELECTORS['wordpress'] + selectors
# 1. Versuch: Domain-spezifische Selektoren
extracted_text = extract_with_selectors(soup, selectors)
if extracted_text and len(extracted_text.split()) > 50:
logging.info(f"🎉 Erfolgreiche Extraktion: {len(extracted_text.split())} Wörter")
return extracted_text
# 2. Versuch: Generische Selektoren
if not extracted_text:
logging.info("🔄 Fallback auf generische Selektoren")
extracted_text = extract_with_selectors(soup, CONTENT_SELECTORS['generic'])
if extracted_text and len(extracted_text.split()) > 50:
logging.info(f"🎉 Erfolgreiche Extraktion (generisch): {len(extracted_text.split())} Wörter")
return extracted_text
# 3. Versuch: Paragraph-Extraktion
if not extracted_text:
logging.info("🔄 Fallback auf Paragraph-Extraktion")
extracted_text = extract_from_paragraphs(soup)
if extracted_text and len(extracted_text.split()) > 30:
logging.info(f"🎉 Erfolgreiche Extraktion (Paragraphen): {len(extracted_text.split())} Wörter")
return extracted_text
# 4. Letzter Versuch: Gesamter Body-Text
if not extracted_text:
logging.info("🔄 Letzter Fallback: Body-Text")
body = soup.find('body')
if body:
# Entferne Navigation, Header, Footer
for element in body(['nav', 'header', 'footer', 'aside', 'script', 'style']):
element.decompose()
body_text = body.get_text(' ', strip=True)
if len(body_text.split()) > 100:
extracted_text = clean_extracted_text(body_text)
logging.info(f"⚠️ Body-Extraktion: {len(extracted_text.split())} Wörter")
return extracted_text
# Kein brauchbarer Text gefunden
if not extracted_text:
logging.warning(f"⚠️ Keine verwertbaren Inhalte gefunden bei: {url}")
return ""
return extracted_text
except requests.RequestException as e:
retries += 1
logging.warning(f"🌐 Netzwerkfehler bei {url} (Versuch {retries}): {e}")
if retries < MAX_RETRIES:
time.sleep(2 ** retries) # Exponential backoff
continue
else:
logging.error(f"❌ Maximale Anzahl Versuche erreicht für: {url}")
return ""
except Exception as e:
logging.error(f"❌ Unerwarteter Fehler bei Volltextextraktion von {url}: {e}")
return ""
return ""
def extract_article_summary(full_text: str, max_length: int = 300) -> str:
"""
Erstellt eine intelligente Zusammenfassung aus dem Volltext
"""
if not full_text:
return ""
sentences = full_text.split('.')
# Erste 2-3 sinnvolle Sätze als Summary verwenden
summary_sentences = []
current_length = 0
for sentence in sentences[:5]: # Maximal erste 5 Sätze prüfen
sentence = sentence.strip()
if len(sentence) < 20: # Zu kurze Sätze überspringen
continue
if current_length + len(sentence) > max_length:
break
summary_sentences.append(sentence)
current_length += len(sentence)
summary = '. '.join(summary_sentences)
if summary and not summary.endswith('.'):
summary += '.'
return summary[:max_length]
def validate_extracted_content(text: str) -> bool:
"""
Validiert ob der extrahierte Inhalt brauchbar ist
"""
if not text or len(text.strip()) < 100:
return False
words = text.split()
# Mindestens 50 Wörter
if len(words) < 50:
return False
# Nicht zu viele Sonderzeichen (Navigation etc.)
special_chars = len([c for c in text if c in '|•→←↑↓'])
if special_chars > len(text) * 0.05: # Mehr als 5% Sonderzeichen
return False
# Durchschnittliche Wortlänge prüfen (zu kurz = Navigation)
avg_word_length = sum(len(word) for word in words) / len(words)
if avg_word_length < 3:
return False
return True

View file

@ -2,59 +2,325 @@
import requests import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from urllib.parse import urljoin from urllib.parse import urljoin, urlparse
import logging import logging
import time
from typing import List, Dict
# Konfiguration
MAX_IMAGES = 5
MIN_IMAGE_SIZE = 100 # Mindestgröße in Pixeln
ALLOWED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.webp', '.gif'}
REQUEST_TIMEOUT = 10
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
def extract_images_with_metadata(article_url): def is_valid_image_url(url: str) -> bool:
""" """
Versucht, Bilder mit Bildunterschrift und Copyright aus dem Originalartikel zu extrahieren. Prüft ob eine URL auf ein gültiges Bild zeigt
Gibt eine Liste mit Dictionaries zurück: {url, alt, copyright, copyright_url, caption} """
try:
parsed = urlparse(url)
path = parsed.path.lower()
# Prüfe Dateiendung
if not any(path.endswith(ext) for ext in ALLOWED_EXTENSIONS):
return False
# Prüfe ob URL vollständig ist
if not parsed.scheme or not parsed.netloc:
return False
# Blacklist für unerwünschte Bilder
blacklist_patterns = [
'avatar', 'profile', 'icon', 'logo', 'banner',
'advertisement', 'ads', 'tracking', 'pixel', 'social'
]
return not any(pattern in url.lower() for pattern in blacklist_patterns)
except Exception:
return False
def get_image_dimensions(img_tag) -> tuple:
"""
Versucht die Bildabmessungen aus HTML-Attributen zu ermitteln
"""
try:
width = img_tag.get('width')
height = img_tag.get('height')
if width and height:
return int(width), int(height)
# Aus Style-Attribut extrahieren
style = img_tag.get('style', '')
if 'width:' in style or 'height:' in style:
# Vereinfachte Extraktion - könnte erweitert werden
pass
return None, None
except:
return None, None
def extract_image_metadata(img_tag, base_url: str) -> Dict:
"""
Extrahiert alle verfügbaren Metadaten eines Bildes
"""
try:
# Basis-URL
src = img_tag.get('src') or img_tag.get('data-src') or img_tag.get('data-lazy-src')
if not src:
return None
img_url = urljoin(base_url, src)
if not is_valid_image_url(img_url):
return None
# Alt-Text
alt_text = img_tag.get('alt', '').strip()
# Titel
title = img_tag.get('title', '').strip()
# Bildabmessungen
width, height = get_image_dimensions(img_tag)
# Überspringe sehr kleine Bilder
if width and height and (width < MIN_IMAGE_SIZE or height < MIN_IMAGE_SIZE):
return None
# Caption und Copyright aus Parent-Elementen suchen
caption = ""
copyright_text = "Unbekannt"
copyright_url = base_url
# Suche in Parent-Elementen nach Caption
parent = img_tag.find_parent(['figure', 'div', 'span', 'p'])
if parent:
# Figcaption
figcaption = parent.find('figcaption')
if figcaption:
caption = figcaption.get_text(strip=True)
# Copyright-Link in Figcaption suchen
copyright_link = figcaption.find('a')
if copyright_link:
copyright_url = urljoin(base_url, copyright_link.get('href', ''))
copyright_text = copyright_link.get_text(strip=True)
# Alternative: Caption in kleinen Texten unter dem Bild
caption_candidates = parent.find_all(['small', 'em', 'i'], limit=3)
for candidate in caption_candidates:
text = candidate.get_text(strip=True)
if len(text) > 10 and len(text) < 200: # Plausible Caption-Länge
if not caption: # Nur wenn noch keine Caption gefunden
caption = text
# Fallback für Caption
if not caption:
caption = title or alt_text or "Bild aus Originalartikel"
return {
"url": img_url,
"alt": alt_text,
"caption": caption[:300] if caption else "Kein Bildtitel vorhanden",
"copyright": copyright_text or "Unbekannt",
"copyright_url": copyright_url or base_url,
"width": width,
"height": height,
"title": title
}
except Exception as e:
logging.error(f"Fehler bei Metadaten-Extraktion: {e}")
return None
def extract_images_with_metadata(article_url: str) -> List[Dict]:
"""
Hauptfunktion: Extrahiert Bilder mit Metadaten aus einem Artikel
""" """
images = [] images = []
if not article_url:
return images
try: try:
logging.info(f"📷 Extrahiere Bilder von {article_url}") logging.info(f"🖼️ Starte Bildextraktion von: {article_url}")
response = requests.get(article_url, timeout=10)
if response.status_code != 200: # HTTP-Request mit verbessertem Header
logging.warning(f"Keine gültige Antwort von {article_url} (Status {response.status_code})") headers = {
return [] 'User-Agent': USER_AGENT,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
}
response = requests.get(article_url, timeout=REQUEST_TIMEOUT, headers=headers)
response.raise_for_status()
soup = BeautifulSoup(response.content, "html.parser") soup = BeautifulSoup(response.content, "html.parser")
for img_tag in soup.find_all("img"): # Alle img-Tags finden
src = img_tag.get("src") img_tags = soup.find_all("img")
if not src: logging.info(f"🔍 {len(img_tags)} img-Tags gefunden")
continue
img_url = urljoin(article_url, src) processed_urls = set() # Duplikate vermeiden
alt_text = img_tag.get("alt", "").strip()
copyright_text = "Unbekannt" for img_tag in img_tags:
copyright_link = article_url try:
caption = alt_text or "Bild aus Originalartikel" # Metadaten extrahieren
image_data = extract_image_metadata(img_tag, article_url)
parent = img_tag.find_parent(["figure", "div"]) if image_data and image_data["url"] not in processed_urls:
if parent:
figcaption = parent.find("figcaption")
if figcaption:
caption = figcaption.get_text(strip=True)
link_tag = figcaption.find("a")
if link_tag and link_tag.has_attr("href"):
copyright_link = link_tag["href"]
copyright_text = link_tag.get_text(strip=True)
image_data = {
"url": img_url,
"alt": alt_text,
"caption": caption or "Kein Bildtitel vorhanden",
"copyright": copyright_text or "Unbekannt",
"copyright_url": copyright_link or article_url
}
images.append(image_data) images.append(image_data)
processed_urls.add(image_data["url"])
logging.info(f"{len(images)} Bilder gefunden bei {article_url}") logging.info(f"✅ Bild hinzugefügt: {image_data['caption'][:50]}...")
return images
# Maximum erreicht?
if len(images) >= MAX_IMAGES:
break
except Exception as e: except Exception as e:
logging.exception(f"Fehler bei der Bildextraktion aus {article_url}:") logging.error(f"❌ Fehler beim Verarbeiten eines Bildes: {e}")
continue
# Bilder nach Größe sortieren (größere zuerst)
images.sort(key=lambda x: (x.get('width', 0) * x.get('height', 0)), reverse=True)
logging.info(f"🎉 {len(images)} Bilder erfolgreich extrahiert von {article_url}")
return images[:MAX_IMAGES] # Sicherheitshalber nochmal begrenzen
except requests.RequestException as e:
logging.error(f"🌐 Netzwerkfehler bei {article_url}: {e}")
return [] return []
except Exception as e:
logging.error(f"❌ Unerwarteter Fehler bei Bildextraktion von {article_url}: {e}")
return []
def validate_image_url(url: str) -> bool:
"""
Prüft ob ein Bild tatsächlich erreichbar ist
"""
try:
response = requests.head(url, timeout=5)
content_type = response.headers.get('content-type', '').lower()
return response.status_code == 200 and 'image' in content_type
except:
return False
def extract_featured_image(article_url: str) -> Dict:
"""
Versucht das Hauptbild/Featured Image eines Artikels zu finden
"""
try:
headers = {'User-Agent': USER_AGENT}
response = requests.get(article_url, timeout=REQUEST_TIMEOUT, headers=headers)
response.raise_for_status()
soup = BeautifulSoup(response.content, "html.parser")
# OpenGraph Image
og_image = soup.find('meta', property='og:image')
if og_image and og_image.get('content'):
img_url = urljoin(article_url, og_image['content'])
if is_valid_image_url(img_url):
return {
"url": img_url,
"alt": "Featured Image",
"caption": "Hauptbild des Artikels",
"copyright": "Unbekannt",
"copyright_url": article_url,
"type": "featured"
}
# Twitter Card Image
twitter_image = soup.find('meta', attrs={'name': 'twitter:image'})
if twitter_image and twitter_image.get('content'):
img_url = urljoin(article_url, twitter_image['content'])
if is_valid_image_url(img_url):
return {
"url": img_url,
"alt": "Featured Image",
"caption": "Hauptbild des Artikels",
"copyright": "Unbekannt",
"copyright_url": article_url,
"type": "featured"
}
return None
except Exception as e:
logging.error(f"Fehler bei Featured Image Extraktion: {e}")
return None
def clean_image_metadata(images: List[Dict]) -> List[Dict]:
"""
Bereinigt und normalisiert Bildmetadaten
"""
cleaned_images = []
for img in images:
try:
# URL validieren
if not img.get("url") or not is_valid_image_url(img["url"]):
continue
# Metadaten bereinigen
cleaned_img = {
"url": img["url"].strip(),
"alt": (img.get("alt") or "").strip()[:200],
"caption": (img.get("caption") or "Kein Bildtitel vorhanden").strip()[:300],
"copyright": (img.get("copyright") or "Unbekannt").strip()[:100],
"copyright_url": (img.get("copyright_url") or "#").strip(),
"width": img.get("width"),
"height": img.get("height"),
"title": (img.get("title") or "").strip()[:200]
}
# Leere Felder mit Standardwerten füllen
if not cleaned_img["caption"]:
cleaned_img["caption"] = "Kein Bildtitel vorhanden"
if not cleaned_img["copyright"]:
cleaned_img["copyright"] = "Unbekannt"
if not cleaned_img["copyright_url"] or cleaned_img["copyright_url"] == "#":
cleaned_img["copyright_url"] = img["url"] # Bild-URL als Fallback
cleaned_images.append(cleaned_img)
except Exception as e:
logging.error(f"Fehler beim Bereinigen der Bildmetadaten: {e}")
continue
return cleaned_images
# Hauptfunktion für bessere Kompatibilität mit dem bestehenden Code
def extract_images_with_metadata_enhanced(article_url: str) -> List[Dict]:
"""
Erweiterte Bildextraktion mit Fallback-Strategien
"""
all_images = []
# 1. Featured Image versuchen
featured = extract_featured_image(article_url)
if featured:
all_images.append(featured)
# 2. Normale Bildextraktion
content_images = extract_images_with_metadata(article_url)
all_images.extend(content_images)
# 3. Duplikate entfernen
seen_urls = set()
unique_images = []
for img in all_images:
if img["url"] not in seen_urls:
unique_images.append(img)
seen_urls.add(img["url"])
# 4. Metadaten bereinigen
cleaned_images = clean_image_metadata(unique_images)
return cleaned_images[:MAX_IMAGES]

236
utils/ui_helpers.py Normal file
View file

@ -0,0 +1,236 @@
# utils/ui_helpers.py
import streamlit as st
from datetime import datetime
import logging
def show_toast(message, type="success", duration=3):
"""
Zeigt eine Toast-Benachrichtigung an
"""
if type == "success":
st.success(message)
elif type == "error":
st.error(message)
elif type == "warning":
st.warning(message)
elif type == "info":
st.info(message)
def format_datetime(date_str):
"""
Formatiert Datetime-Strings für bessere Lesbarkeit
"""
try:
if isinstance(date_str, str):
if "GMT" in date_str or "+" in date_str:
dt = datetime.strptime(date_str, "%a, %d %b %Y %H:%M:%S %z")
return dt.strftime("%d.%m.%Y %H:%M")
elif "T" in date_str:
dt = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
return dt.strftime("%d.%m.%Y %H:%M")
else:
return date_str[:16].replace("T", " ")
return str(date_str)
except Exception as e:
logging.warning(f"Datum konnte nicht formatiert werden: {date_str} - {e}")
return str(date_str)[:16]
def get_status_color(status):
"""
Gibt die passende Farbe für einen Status zurück
"""
colors = {
"New": "#2196f3",
"Rewrite": "#ff9800",
"Process": "#9c27b0",
"Online": "#4caf50",
"On Hold": "#e91e63",
"Trash": "#f44336"
}
return colors.get(status, "#2196f3")
def create_status_badge(status):
"""
Erstellt einen HTML-Status-Badge
"""
color = get_status_color(status)
return f"""
<span style="
background-color: {color}20;
color: {color};
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
border: 1px solid {color}40;
">{status}</span>
"""
def truncate_text(text, max_length=150):
"""
Kürzt Text auf maximale Länge
"""
if not text:
return ""
if len(text) <= max_length:
return text
return text[:max_length].rsplit(' ', 1)[0] + "..."
def calculate_reading_time(text):
"""
Berechnet geschätzte Lesezeit (200 Wörter/Minute)
"""
if not text:
return 0
word_count = len(text.split())
reading_time = max(1, word_count // 200)
return reading_time
def validate_url(url):
"""
Validiert eine URL
"""
import re
pattern = re.compile(
r'^https?://' # http:// oder https://
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' # domain...
r'localhost|' # localhost...
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...oder IP
r'(?::\d+)?' # optional port
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
return pattern.match(url) is not None
def create_article_card_html(article, source_name="Unbekannt"):
"""
Erstellt HTML für eine Artikel-Karte
"""
has_images = len(article.get("images", [])) > 0
word_count = len(article.get("text", "").split())
reading_time = calculate_reading_time(article.get("text", ""))
# Unvollständige Bilder prüfen
incomplete_images = any(
not all(k in img and img[k] for k in ("caption", "copyright", "copyright_url"))
for img in article.get("images", [])
)
warning_icon = " ⚠️" if incomplete_images else ""
return f"""
<div style="
background: white;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
border-left: 4px solid {get_status_color(article.get('status', 'New'))};
transition: transform 0.2s ease;
" onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform='translateY(0)'">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
<div style="flex: 1;">
<h3 style="margin: 0 0 0.5rem 0; color: #333; font-size: 1.1rem;">
{article.get('title', 'Kein Titel')}{warning_icon}
</h3>
<div style="font-size: 0.85rem; color: #666; margin-bottom: 0.5rem;">
📅 {format_datetime(article.get('date', ''))}
📝 {word_count} Wörter
{reading_time} Min Lesezeit
{'• 🖼️ ' + str(len(article.get('images', []))) + ' Bilder' if has_images else ''}
</div>
</div>
<div>
{create_status_badge(article.get('status', 'New'))}
</div>
</div>
<div style="margin-bottom: 1rem; color: #555; line-height: 1.4;">
{truncate_text(article.get('summary', ''), 200)}
</div>
<div style="display: flex; justify-content: space-between; align-items: center; font-size: 0.8rem; color: #888;">
<div>
📡 {source_name}
</div>
<div>
🏷 {', '.join(article.get('tags', [])[:3])}{'...' if len(article.get('tags', [])) > 3 else ''}
</div>
</div>
</div>
"""
def create_stats_card(title, value, icon="📊", color="#667eea"):
"""
Erstellt eine Statistik-Karte
"""
return f"""
<div style="
background: white;
border-radius: 12px;
padding: 1.5rem;
text-align: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
border-top: 4px solid {color};
">
<div style="font-size: 2rem; margin-bottom: 0.5rem;">{icon}</div>
<div style="font-size: 2rem; font-weight: bold; color: {color}; margin-bottom: 0.5rem;">{value}</div>
<div style="color: #666; font-weight: 500;">{title}</div>
</div>
"""
def show_loading_spinner(text="Lädt..."):
"""
Zeigt einen Lade-Spinner mit Text
"""
return st.empty().markdown(f"""
<div style="text-align: center; padding: 2rem;">
<div style="
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 1rem auto;
"></div>
<div style="color: #666;">{text}</div>
</div>
<style>
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
""", unsafe_allow_html=True)
def create_filter_section():
"""
Erstellt einen modernen Filter-Bereich
"""
return """
<div style="
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 2rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
">
<h3 style="margin: 0 0 1rem 0; color: #333;">🔍 Filter & Suche</h3>
"""
def get_error_message(error_type, details=""):
"""
Gibt formatierte Fehlermeldungen zurück
"""
messages = {
"feed_error": f"❌ Fehler beim Laden des Feeds: {details}",
"save_error": f"❌ Fehler beim Speichern: {details}",
"api_error": f"❌ API-Fehler: {details}",
"validation_error": f"⚠️ Validierungsfehler: {details}",
"network_error": f"🌐 Netzwerkfehler: {details}"
}
return messages.get(error_type, f"❌ Unbekannter Fehler: {details}")

468
utils/wordpress_uploader.py Normal file
View file

@ -0,0 +1,468 @@
# utils/wordpress_uploader.py
import requests
import json
import os
import logging
import base64
from datetime import datetime
from typing import Dict, List, Optional, Tuple
from dotenv import load_dotenv
load_dotenv()
# WordPress API Konfiguration
WP_BASE_URL = os.getenv("WP_BASE_URL", "https://vanityontour.de")
WP_USERNAME = os.getenv("WP_USERNAME", "ogiertz")
WP_PASSWORD = os.getenv("WP_PASSWORD", "whNEx9aZCIUXViV89Z3e7Z03")
WP_AUTH_BASE64 = os.getenv("WP_AUTH_BASE64", "b2dpZXJ0ejp3aE5FeDlhWkNJVVhWaVY4OVozZTdaMDM=")
WP_API_ENDPOINT = f"{WP_BASE_URL}/wp-json/wp/v2"
# Request-Konfiguration
REQUEST_TIMEOUT = 30
MAX_RETRIES = 3
USER_AGENT = 'RSS-Feed-Manager/1.6.1'
class WordPressUploader:
"""
Klasse für den Upload von Artikeln zu WordPress über die REST API
mit Base64-Authentifizierung
"""
def __init__(self):
self.base_url = WP_BASE_URL
self.api_endpoint = WP_API_ENDPOINT
self.username = WP_USERNAME
self.password = WP_PASSWORD
self.auth_base64 = WP_AUTH_BASE64
# Session für bessere Performance
self.session = requests.Session()
# Authentifizierung über Authorization Header mit Base64
if self.auth_base64:
# Verwende bereitgestellten Base64-String
self.session.headers.update({
'Authorization': f'Basic {self.auth_base64}',
'User-Agent': USER_AGENT,
'Content-Type': 'application/json',
'Accept': 'application/json'
})
logging.info("✅ WordPress-Authentifizierung: Verwende bereitgestellten Base64-String")
else:
# Fallback: Generiere Base64 aus Username/Password
if self.username and self.password:
credentials = f"{self.username}:{self.password}"
encoded_credentials = base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
self.session.headers.update({
'Authorization': f'Basic {encoded_credentials}',
'User-Agent': USER_AGENT,
'Content-Type': 'application/json',
'Accept': 'application/json'
})
logging.info("✅ WordPress-Authentifizierung: Base64 aus Username/Password generiert")
else:
logging.error("❌ WordPress-Authentifizierung: Weder Base64-String noch Username/Password verfügbar")
raise ValueError("WordPress-Authentifizierung nicht konfiguriert")
# Standard-Kategorie ID ermitteln
self.default_category_id = self._get_default_category_id()
def _get_default_category_id(self) -> int:
"""
Ermittelt die ID der Standard-Kategorie 'Allgemein'
"""
try:
response = self.session.get(
f"{self.api_endpoint}/categories",
params={'search': 'Allgemein', 'per_page': 10},
timeout=REQUEST_TIMEOUT
)
response.raise_for_status()
categories = response.json()
for category in categories:
if category['name'].lower() == 'allgemein':
logging.info(f"✅ Standard-Kategorie 'Allgemein' gefunden: ID {category['id']}")
return category['id']
# Fallback: Erste Kategorie oder Standard-ID
if categories:
logging.warning(f"⚠️ Kategorie 'Allgemein' nicht gefunden, verwende '{categories[0]['name']}' (ID: {categories[0]['id']})")
return categories[0]['id']
else:
logging.warning("⚠️ Keine Kategorien gefunden, verwende Standard-ID 1")
return 1
except Exception as e:
logging.error(f"❌ Fehler beim Ermitteln der Standard-Kategorie: {e}")
return 1 # WordPress Standard-Kategorie
def _get_or_create_tags(self, tag_names: List[str]) -> List[int]:
"""
Ermittelt oder erstellt Tags und gibt deren IDs zurück
"""
tag_ids = []
if not tag_names:
return tag_ids
try:
# Bestehende Tags abrufen
for tag_name in tag_names:
tag_name = tag_name.strip()
if not tag_name:
continue
try:
# Suche nach existierendem Tag
response = self.session.get(
f"{self.api_endpoint}/tags",
params={'search': tag_name, 'per_page': 10},
timeout=REQUEST_TIMEOUT
)
response.raise_for_status()
existing_tags = response.json()
tag_found = False
# Exakte Übereinstimmung suchen
for tag in existing_tags:
if tag['name'].lower() == tag_name.lower():
tag_ids.append(tag['id'])
tag_found = True
logging.info(f"✅ Existierender Tag gefunden: '{tag_name}' (ID: {tag['id']})")
break
# Tag erstellen falls nicht gefunden
if not tag_found:
create_response = self.session.post(
f"{self.api_endpoint}/tags",
json={'name': tag_name},
timeout=REQUEST_TIMEOUT
)
if create_response.status_code == 201:
new_tag = create_response.json()
tag_ids.append(new_tag['id'])
logging.info(f"✅ Neuer Tag erstellt: '{tag_name}' (ID: {new_tag['id']})")
else:
logging.warning(f"⚠️ Tag '{tag_name}' konnte nicht erstellt werden: {create_response.status_code}")
continue
except Exception as e:
logging.error(f"❌ Fehler beim Verarbeiten von Tag '{tag_name}': {e}")
continue
logging.info(f"🏷️ Tags verarbeitet: {len(tag_ids)} Tag-IDs erstellt")
return tag_ids
except Exception as e:
logging.error(f"❌ Allgemeiner Fehler bei Tag-Verarbeitung: {e}")
return []
def _prepare_post_data(self, article: Dict) -> Dict:
"""
Bereitet die Artikel-Daten für WordPress vor
"""
# Tags verarbeiten - WordPress benötigt Tag-IDs, nicht Namen
tag_names = article.get('tags', [])
tag_ids = self._get_or_create_tags(tag_names)
# Basis Post-Daten
post_data = {
'title': article.get('title', 'Kein Titel'),
'content': article.get('text', ''),
'status': 'pending', # Artikel als "Ausstehend" markieren
'categories': [self.default_category_id],
'excerpt': article.get('summary', '')[:300], # WordPress Excerpt
'meta': {
'rss_source': article.get('source', ''),
'rss_original_link': article.get('link', ''),
'rss_import_date': datetime.now().isoformat(),
'rss_article_id': article.get('id', '')
}
}
# Tags nur hinzufügen wenn vorhanden
if tag_ids:
post_data['tags'] = tag_ids
# Optional: Author setzen (falls unterschiedliche Autoren gewünscht)
# post_data['author'] = 1 # WordPress User ID
logging.info(f"📝 Post-Daten vorbereitet: Titel='{post_data['title']}', Tags={len(tag_ids)}, Kategorie={self.default_category_id}")
return post_data
def _check_duplicate(self, article: Dict) -> Optional[int]:
"""
Prüft, ob ein Artikel bereits in WordPress existiert
"""
try:
# Suche nach Titel
title = article.get('title', '')
if not title:
return None
response = self.session.get(
f"{self.api_endpoint}/posts",
params={
'search': title,
'per_page': 5,
'status': 'any' # Alle Status durchsuchen
},
timeout=REQUEST_TIMEOUT
)
response.raise_for_status()
posts = response.json()
for post in posts:
# Exakte Titel-Übereinstimmung
if post['title']['rendered'].strip() == title.strip():
logging.info(f"🔄 Duplikat gefunden: '{title}' (WordPress ID: {post['id']})")
return post['id']
# Prüfe auch Custom Meta Fields (RSS Article ID)
article_id = article.get('id')
if article_id:
# Meta-Felder würden eine separate API-Abfrage erfordern
# Für jetzt: Nur Titel-basierte Duplikatserkennung
pass
return None
except Exception as e:
logging.error(f"❌ Fehler bei Duplikatsprüfung für '{article.get('title', 'Unbekannt')}': {e}")
return None
def upload_article(self, article: Dict) -> Tuple[bool, str, Optional[int]]:
"""
Lädt einen einzelnen Artikel zu WordPress hoch
Returns:
Tuple[bool, str, Optional[int]]: (Erfolg, Nachricht, WordPress Post ID)
"""
title = article.get('title', 'Unbekannt')
try:
logging.info(f"📤 Starte WordPress-Upload: {title}")
# Duplikatsprüfung
existing_post_id = self._check_duplicate(article)
if existing_post_id:
return False, f"Artikel '{title}' existiert bereits in WordPress (ID: {existing_post_id})", existing_post_id
# Post-Daten vorbereiten
post_data = self._prepare_post_data(article)
# Upload mit Retry-Logik
for attempt in range(MAX_RETRIES):
try:
response = self.session.post(
f"{self.api_endpoint}/posts",
json=post_data,
timeout=REQUEST_TIMEOUT
)
if response.status_code == 201:
# Erfolgreich erstellt
wp_post = response.json()
wp_post_id = wp_post['id']
wp_url = wp_post['link']
logging.info(f"✅ WordPress-Upload erfolgreich: '{title}' (ID: {wp_post_id})")
logging.info(f"🔗 WordPress-URL: {wp_url}")
return True, f"Erfolgreich hochgeladen: {wp_url}", wp_post_id
elif response.status_code == 400:
# Client Error - nicht wiederholen
error_data = response.json()
error_msg = error_data.get('message', 'Unbekannter Fehler')
error_code = error_data.get('code', 'unknown')
# Detaillierte Fehleranalyse
if 'parameter' in error_msg.lower() and 'tags' in error_msg.lower():
logging.error(f"❌ WordPress-Tag-Fehler für '{title}': {error_msg}")
logging.error(f"📋 Post-Daten: {json.dumps(post_data, indent=2, ensure_ascii=False)}")
return False, f"Tag-Fehler: {error_msg} (Artikel-Tags: {article.get('tags', [])})", None
else:
logging.error(f"❌ WordPress-Fehler 400 für '{title}': {error_msg} (Code: {error_code})")
logging.error(f"📋 Post-Daten: {json.dumps(post_data, indent=2, ensure_ascii=False)}")
return False, f"WordPress-Fehler: {error_msg}", None
elif response.status_code == 401:
# Authentifizierungsfehler
logging.error(f"❌ WordPress-Authentifizierungsfehler für '{title}'")
return False, "Authentifizierungsfehler - bitte Zugangsdaten prüfen", None
elif response.status_code == 403:
# Berechtigungsfehler
logging.error(f"❌ WordPress-Berechtigungsfehler für '{title}'")
return False, "Keine Berechtigung zum Erstellen von Posts", None
else:
# Server Error - Retry möglich
if attempt < MAX_RETRIES - 1:
logging.warning(f"⚠️ WordPress-Upload Versuch {attempt + 1} fehlgeschlagen für '{title}' (Status: {response.status_code}), versuche erneut...")
continue
else:
logging.error(f"❌ WordPress-Upload nach {MAX_RETRIES} Versuchen fehlgeschlagen für '{title}' (Status: {response.status_code})")
return False, f"Upload fehlgeschlagen nach {MAX_RETRIES} Versuchen (HTTP {response.status_code})", None
except requests.exceptions.Timeout:
if attempt < MAX_RETRIES - 1:
logging.warning(f"⏱️ Timeout bei WordPress-Upload für '{title}' (Versuch {attempt + 1}), versuche erneut...")
continue
else:
logging.error(f"❌ Timeout bei WordPress-Upload für '{title}' nach {MAX_RETRIES} Versuchen")
return False, f"Timeout nach {MAX_RETRIES} Versuchen", None
except requests.exceptions.ConnectionError as e:
if attempt < MAX_RETRIES - 1:
logging.warning(f"🌐 Verbindungsfehler bei WordPress-Upload für '{title}' (Versuch {attempt + 1}): {e}")
continue
else:
logging.error(f"❌ Verbindungsfehler bei WordPress-Upload für '{title}' nach {MAX_RETRIES} Versuchen: {e}")
return False, f"Verbindungsfehler nach {MAX_RETRIES} Versuchen", None
except Exception as e:
logging.error(f"❌ Unerwarteter Fehler bei WordPress-Upload für '{title}': {e}")
return False, f"Unerwarteter Fehler: {str(e)}", None
def test_connection(self) -> Tuple[bool, str]:
"""
Testet die Verbindung zur WordPress API mit Base64-Authentifizierung
"""
try:
logging.info("🔧 Teste WordPress-API-Verbindung mit Base64-Auth...")
# Debug: Auth-Header prüfen
auth_header = self.session.headers.get('Authorization', 'Nicht gesetzt')
logging.info(f"🔑 Authorization Header: {auth_header[:20]}..." if len(auth_header) > 20 else f"🔑 Authorization Header: {auth_header}")
# Einfache Abfrage der Kategorien als Test
response = self.session.get(
f"{self.api_endpoint}/categories",
params={'per_page': 1},
timeout=REQUEST_TIMEOUT
)
logging.info(f"📡 API-Response Status: {response.status_code}")
logging.info(f"📡 API-Response Headers: {dict(response.headers)}")
if response.status_code == 200:
logging.info("✅ WordPress-API-Verbindung erfolgreich")
return True, "Verbindung zur WordPress API erfolgreich"
elif response.status_code == 401:
logging.error("❌ WordPress-API-Authentifizierung fehlgeschlagen")
logging.error(f"Response Body: {response.text}")
return False, "Authentifizierung fehlgeschlagen - bitte Base64-String oder Zugangsdaten prüfen"
elif response.status_code == 403:
logging.error("❌ WordPress-API-Berechtigung fehlgeschlagen")
logging.error(f"Response Body: {response.text}")
return False, "Keine Berechtigung - bitte Benutzerrechte prüfen"
else:
logging.error(f"❌ WordPress-API-Test fehlgeschlagen (Status: {response.status_code})")
logging.error(f"Response Body: {response.text}")
return False, f"API-Test fehlgeschlagen (HTTP {response.status_code}): {response.text[:100]}"
except requests.exceptions.ConnectionError as e:
logging.error(f"❌ Verbindungsfehler zur WordPress API: {e}")
return False, f"Verbindungsfehler: {str(e)}"
except Exception as e:
logging.error(f"❌ Unerwarteter Fehler beim WordPress-API-Test: {e}")
return False, f"Unerwarteter Fehler: {str(e)}"
def upload_multiple_articles(self, articles: List[Dict]) -> Dict:
"""
Lädt mehrere Artikel zu WordPress hoch
Returns:
Dict mit Statistiken über erfolgreiche und fehlgeschlagene Uploads
"""
results = {
'total': len(articles),
'successful': 0,
'failed': 0,
'duplicates': 0,
'details': []
}
logging.info(f"📦 Starte Batch-Upload von {len(articles)} Artikeln zu WordPress")
for i, article in enumerate(articles, 1):
title = article.get('title', f'Artikel {i}')
logging.info(f"📤 Upload {i}/{len(articles)}: {title}")
success, message, wp_post_id = self.upload_article(article)
result_detail = {
'article_id': article.get('id'),
'title': title,
'success': success,
'message': message,
'wp_post_id': wp_post_id
}
results['details'].append(result_detail)
if success:
results['successful'] += 1
elif 'existiert bereits' in message:
results['duplicates'] += 1
else:
results['failed'] += 1
# Kurze Pause zwischen Uploads
if i < len(articles):
import time
time.sleep(1)
logging.info(f"📊 Batch-Upload abgeschlossen: {results['successful']} erfolgreich, {results['failed']} fehlgeschlagen, {results['duplicates']} Duplikate")
return results
def __del__(self):
"""
Session sauber schließen
"""
if hasattr(self, 'session'):
self.session.close()
def upload_articles_to_wordpress(articles: List[Dict]) -> Dict:
"""
Convenience-Funktion für den Upload von Artikeln zu WordPress
"""
uploader = WordPressUploader()
# Verbindung testen
connection_ok, connection_msg = uploader.test_connection()
if not connection_ok:
logging.error(f"❌ WordPress-Verbindung fehlgeschlagen: {connection_msg}")
return {
'total': len(articles),
'successful': 0,
'failed': len(articles),
'duplicates': 0,
'error': connection_msg,
'details': []
}
# Artikel hochladen
return uploader.upload_multiple_articles(articles)
def upload_single_article_to_wordpress(article: Dict) -> Tuple[bool, str, Optional[int]]:
"""
Convenience-Funktion für den Upload eines einzelnen Artikels
"""
uploader = WordPressUploader()
# Verbindung testen
connection_ok, connection_msg = uploader.test_connection()
if not connection_ok:
return False, connection_msg, None
# Artikel hochladen
return uploader.upload_article(article)