commit
1c63163f22
11 changed files with 7172 additions and 516 deletions
933
CHANGELOG.md
933
CHANGELOG.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
VERSION = "1.5.3"
|
VERSION = "1.6.2"
|
||||||
|
|
|
||||||
3086
data/articles.json
3086
data/articles.json
File diff suppressed because one or more lines are too long
20
internal/git.sh
Normal file
20
internal/git.sh
Normal 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
|
||||||
|
|
@ -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
463
main.py
|
|
@ -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 {}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
236
utils/ui_helpers.py
Normal 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
468
utils/wordpress_uploader.py
Normal 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)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue