UI-Anpassung: Feed-Anzeige entfernt, Tabelle verbessert
This commit is contained in:
parent
fe2191e6c8
commit
2db9544fda
9 changed files with 438 additions and 156 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -24,8 +24,8 @@ Thumbs.db
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# JSON-Datenbanken / Zwischenspeicherung
|
# JSON-Datenbanken / Zwischenspeicherung
|
||||||
*.json
|
*.json
|
||||||
!feeds.json # optional entfernbar, wenn du Beispielfeeds mit versionieren willst
|
!feeds.json # optional entfernbar, wenn du Beispielfeeds mit versionieren willst
|
||||||
|
|
||||||
# LOG Files ausschließen, dienen nur zum Debuggen
|
# LOG Files ausschließen, dienen nur zum Debuggen
|
||||||
# *.log
|
# *.log
|
||||||
|
|
|
||||||
20
CHANGELOG.md
20
CHANGELOG.md
|
|
@ -1,5 +1,25 @@
|
||||||
# CHANGELOG.md
|
# CHANGELOG.md
|
||||||
|
|
||||||
|
## [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
|
## [1.2.0] - 2025-07-04
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
- Automatische Bilderkennung beim Einlesen von Artikeln
|
- Automatische Bilderkennung beim Einlesen von Artikeln
|
||||||
|
|
|
||||||
5
app.log
5
app.log
|
|
@ -1,5 +0,0 @@
|
||||||
2025-07-04 09:29:50,207 - INFO - Status von 1 Artikel(n) auf 'Rewrite' gesetzt.
|
|
||||||
2025-07-04 09:30:17,000 - INFO - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
|
|
||||||
2025-07-04 09:30:17,010 - INFO - ✅ Artikel umgeschrieben: Das weltweit größte Caravaning-Erlebnis
|
|
||||||
2025-07-04 09:46:03,001 - INFO - Status von 1 Artikel(n) auf 'Online' gesetzt.
|
|
||||||
2025-07-04 19:36:22,449 - INFO - 1 neue Artikel geladen.
|
|
||||||
143
app.py
143
app.py
|
|
@ -1,92 +1,95 @@
|
||||||
# app.py
|
# app.py
|
||||||
|
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from dotenv import load_dotenv
|
from main import (
|
||||||
from email.utils import parsedate_to_datetime
|
load_feeds,
|
||||||
from main import load_articles, save_articles, process_articles, fetch_and_process_feed, rewrite_articles
|
save_feeds,
|
||||||
|
load_articles,
|
||||||
|
save_articles,
|
||||||
|
process_articles,
|
||||||
|
rewrite_articles
|
||||||
|
)
|
||||||
|
import os
|
||||||
|
|
||||||
load_dotenv()
|
st.set_page_config(page_title="📰 RSS Artikel Manager", layout="wide")
|
||||||
|
st.title("📰 RSS Artikel Manager")
|
||||||
|
|
||||||
st.set_page_config(layout="wide", page_title="RSS Article Manager")
|
# RSS Feed Verwaltung
|
||||||
|
st.sidebar.header("📡 RSS Feeds verwalten")
|
||||||
|
feeds = load_feeds()
|
||||||
|
new_feed = st.sidebar.text_input("Neuen RSS Feed hinzufügen")
|
||||||
|
if st.sidebar.button("Feed hinzufügen"):
|
||||||
|
if new_feed and new_feed not in feeds:
|
||||||
|
feeds.append({"url": new_feed})
|
||||||
|
save_feeds(feeds)
|
||||||
|
st.sidebar.success("Feed hinzugefügt")
|
||||||
|
|
||||||
# Artikelstatusfilter
|
#if feeds:
|
||||||
status_filter = st.sidebar.selectbox("🔍 Artikelstatus filtern", ["Alle", "New", "Rewrite", "Process", "Online", "On Hold", "Trash"])
|
# st.sidebar.write("### Aktuelle Feeds:")
|
||||||
|
# for feed in feeds:
|
||||||
# Neuen Feed hinzufügen
|
# url = feed["url"] if isinstance(feed, dict) else feed
|
||||||
st.sidebar.markdown("---")
|
# st.sidebar.markdown(f"- {url}")
|
||||||
st.sidebar.header("➕ RSS Feed hinzufügen")
|
#else:
|
||||||
new_feed_url = st.sidebar.text_input("Feed URL")
|
# st.sidebar.info("Noch keine Feeds hinzugefügt.")
|
||||||
if st.sidebar.button("Feed hinzufügen") and new_feed_url:
|
|
||||||
fetch_and_process_feed(new_feed_url)
|
|
||||||
st.rerun()
|
|
||||||
|
|
||||||
# Alle Feeds neu laden
|
|
||||||
if st.sidebar.button("Alle Feeds neu laden"):
|
|
||||||
process_articles()
|
|
||||||
st.rerun()
|
|
||||||
|
|
||||||
# Artikel laden
|
# Artikel laden
|
||||||
try:
|
if st.sidebar.button("🔄 Alle Feeds neu laden"):
|
||||||
articles = load_articles()
|
existing_ids = [a["id"] for a in load_articles()]
|
||||||
except json.decoder.JSONDecodeError:
|
process_articles(existing_ids)
|
||||||
articles = []
|
st.rerun()
|
||||||
|
|
||||||
# Artikel nach Status filtern
|
if st.sidebar.button("✍️ Artikel umschreiben (Rewrite)"):
|
||||||
|
rewrite_articles()
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
# Artikelübersicht
|
||||||
|
st.header("📋 Artikelübersicht")
|
||||||
|
status_filter = st.selectbox("Status filtern", ["Alle", "New", "Rewrite", "Process", "Online", "On Hold", "Trash"])
|
||||||
|
|
||||||
|
articles = load_articles()
|
||||||
if status_filter != "Alle":
|
if status_filter != "Alle":
|
||||||
articles = [a for a in articles if a.get("status") == status_filter]
|
articles = [a for a in articles if a.get("status") == status_filter]
|
||||||
|
|
||||||
# Artikelübersicht
|
# Tabelle anzeigen
|
||||||
st.title("📰 RSS Artikel Übersicht")
|
if articles:
|
||||||
st.markdown("---")
|
st.markdown("### 📄 Übersichtstabelle")
|
||||||
|
st.write("**Spaltenübersicht:** Auswahl | Datum | Titel | Zusammenfassung | Wörter | Tags | Status")
|
||||||
if not articles:
|
|
||||||
st.info("Keine Artikel gefunden.")
|
|
||||||
else:
|
|
||||||
st.markdown("### 📄 Artikelliste")
|
|
||||||
selected_ids = []
|
|
||||||
all_statuses = ["New", "Rewrite", "Process", "Online", "On Hold", "Trash"]
|
|
||||||
|
|
||||||
for article in articles:
|
for article in articles:
|
||||||
col1, col2, col3, col4, col5, col6, col7 = st.columns([0.5, 1.2, 2.5, 2, 1, 2, 1.2])
|
cols = st.columns([0.05, 0.1, 0.2, 0.25, 0.05, 0.2, 0.15])
|
||||||
with col1:
|
with cols[0]:
|
||||||
if st.checkbox("", key=f"select_{article['id']}"):
|
st.checkbox("", key=f"select_{article['id']}")
|
||||||
selected_ids.append(article['id'])
|
with cols[1]:
|
||||||
with col2:
|
st.markdown(datetime.strptime(article["date"], "%a, %d %b %Y %H:%M:%S %z").strftime("%d.%m.%y") if "GMT" in article["date"] or "+" in article["date"] else article["date"][:10])
|
||||||
date = parsedate_to_datetime(article['date']).strftime("%d.%m.%y")
|
with cols[2]:
|
||||||
st.markdown(date)
|
st.markdown(f"**{article['title']}**")
|
||||||
with col3:
|
with cols[3]:
|
||||||
st.markdown(article['title'])
|
st.markdown(article.get("summary", "")[:150])
|
||||||
with col4:
|
with cols[4]:
|
||||||
st.markdown(article['summary'][:150] + ("..." if len(article['summary']) > 150 else ""))
|
st.markdown(str(len(article.get("text", "").split())))
|
||||||
with col5:
|
with cols[5]:
|
||||||
word_count = len(article['text'].split())
|
|
||||||
st.markdown(str(word_count))
|
|
||||||
with col6:
|
|
||||||
st.markdown(", ".join(article.get("tags", [])))
|
st.markdown(", ".join(article.get("tags", [])))
|
||||||
with col7:
|
with cols[6]:
|
||||||
status = st.selectbox("", all_statuses, index=all_statuses.index(article.get("status", "New")), key=f"status_{article['id']}")
|
status_options = ["New", "Rewrite", "Process", "Online", "On Hold", "Trash"]
|
||||||
if status != article.get("status"):
|
current_status = article.get("status", "New")
|
||||||
article["status"] = status
|
new_status = st.selectbox("", status_options, index=status_options.index(current_status), key=f"status_{article['id']}")
|
||||||
|
if new_status != current_status:
|
||||||
|
article["status"] = new_status
|
||||||
save_articles(articles)
|
save_articles(articles)
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
if selected_ids:
|
with st.expander(f"🔍 {article['title']}"):
|
||||||
new_status = st.selectbox("Status für ausgewählte Artikel setzen", all_statuses)
|
st.markdown("#### ✍️ Artikeltext")
|
||||||
if st.button("✅ Status aktualisieren"):
|
st.code(f"{article['title']}\n\n{article['text']}\n\nQuelle: {article['link']}", language="markdown")
|
||||||
for article in articles:
|
|
||||||
if article['id'] in selected_ids:
|
|
||||||
article['status'] = new_status
|
|
||||||
save_articles(articles)
|
|
||||||
st.success("Status aktualisiert.")
|
|
||||||
st.rerun()
|
|
||||||
|
|
||||||
st.markdown("---")
|
st.markdown("#### 🏷️ Tags")
|
||||||
|
st.code(", ".join(article.get("tags", [])), language="markdown")
|
||||||
|
|
||||||
if st.button("✍️ Artikel mit Status 'Rewrite' umschreiben"):
|
st.markdown("#### 🖼️ Bilder")
|
||||||
rewrite_articles()
|
for img in article.get("images", []):
|
||||||
st.rerun()
|
st.image(img["url"], caption=img.get("caption", "Kein Titel"), use_column_width=True)
|
||||||
|
st.caption(f"© {img.get('copyright', 'Unbekannt')} | [Quelle]({img.get('copyright_url', '#')})")
|
||||||
|
|
||||||
st.markdown("---")
|
else:
|
||||||
|
st.info("Keine Artikel für den gewählten Status gefunden.")
|
||||||
|
|
|
||||||
154
data/articles.json
Normal file
154
data/articles.json
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "Artikel 5926 von camping-news.de",
|
||||||
|
"title": "Das weltweit gr\u00f6\u00dfte Caravaning-Erlebnis",
|
||||||
|
"date": "Tue, 13 May 2025 00:00:00 +0200",
|
||||||
|
"summary": "Vom 29. August (Preview Day) bis zum 7. September 2025 wird Düsseldorf erneut zum Treffpunkt der Caravaning-Welt",
|
||||||
|
"text": "Der CARAVAN SALON ist die f\u00fchrende Messe f\u00fcr Caravaning weltweit und bietet eine gro\u00dfe Auswahl an Reisemobilen, Caravans und Campervans. Erg\u00e4nzt wird das Angebot durch eine Vielzahl von Zubeh\u00f6r, technischem Equipment und Dienstleistungen im Bereich Caravaning und Camping. Auch Campingpl\u00e4tze und Reisemobilstellpl\u00e4tze werden vorgestellt. Diese Messe ist ein unerl\u00e4sslicher Treffpunkt der Branche, bei dem Kunden die M\u00f6glichkeit haben, Fahrzeuge vor dem Kauf zu erleben und zu vergleichen.\n\nDie Messe pr\u00e4sentiert zudem die Premieren der kommenden Saison, darunter Top-Produkte, Innovationen und Weltneuheiten, die erstmals \u00f6ffentlich zug\u00e4nglich sind. Dank einer neuen Hallenstruktur k\u00f6nnen Besucher schnell und gezielt ihr Interessengebiet erreichen, egal ob sie Camping-Neulinge, Technik-Fans, kreative Selbstausbauer oder Luxus-Liebhaber sind.\n\nCaravaning erfreut sich seit vielen Jahren wachsender Beliebtheit aufgrund der individuellen und flexiblen Reiseform, die ein selbstbestimmter Urlaub im Einklang mit der Natur erm\u00f6glicht. Dieser Trend hat Caravaning zu einer der beliebtesten Reiseformen weltweit gemacht.\n\nAm 29. August findet der Preview Day statt, an dem die Neuheiten der Branche in Ruhe entdeckt werden k\u00f6nnen. Der Ticketverkauf startet am 11. Juni. Weitere Informationen sind auf der offiziellen Website der Messe erh\u00e4ltlich.",
|
||||||
|
"tags": [
|
||||||
|
"#Caravan Salon",
|
||||||
|
"#Camping Messe",
|
||||||
|
"#Reisemobile"
|
||||||
|
],
|
||||||
|
"status": "Online",
|
||||||
|
"link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5926",
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"url": "https://www.camping-in-deutschland.de/pics2020/logo_weiss.png",
|
||||||
|
"alt": "Bild aus Originalartikel",
|
||||||
|
"copyright_text": "Unbekannt",
|
||||||
|
"copyright_link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5926"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.camping-in-deutschland.de/img/news13/caravan24_ct7020.jpg",
|
||||||
|
"alt": "Bild aus Originalartikel",
|
||||||
|
"copyright_text": "Unbekannt",
|
||||||
|
"copyright_link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5926"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.camping-in-deutschland.de/pics2013/scrollbox-shadow.png",
|
||||||
|
"alt": "Bild aus Originalartikel",
|
||||||
|
"copyright_text": "Unbekannt",
|
||||||
|
"copyright_link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5926"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.camping-in-deutschland.de/pics2013/scrollbox-shadow.png",
|
||||||
|
"alt": "Bild aus Originalartikel",
|
||||||
|
"copyright_text": "Unbekannt",
|
||||||
|
"copyright_link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5926"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.camping-in-deutschland.de/pics2013/scrollbox-shadow.png",
|
||||||
|
"alt": "Bild aus Originalartikel",
|
||||||
|
"copyright_text": "Unbekannt",
|
||||||
|
"copyright_link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5926"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.camping-in-deutschland.de/pics2013/scrollbox-shadow.png",
|
||||||
|
"alt": "Bild aus Originalartikel",
|
||||||
|
"copyright_text": "Unbekannt",
|
||||||
|
"copyright_link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5926"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.hundefreundliche-campingplaetze.de/logos/Familienfreundliche.png",
|
||||||
|
"alt": "Bild aus Originalartikel",
|
||||||
|
"copyright_text": "Unbekannt",
|
||||||
|
"copyright_link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5926"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.camping-in-deutschland.de/camp_bilder/2938.345.thb.jpg",
|
||||||
|
"alt": "Bild aus Originalartikel",
|
||||||
|
"copyright_text": "Unbekannt",
|
||||||
|
"copyright_link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5926"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.camping-in-deutschland.de/camp_bilder/1839.345.thb.jpg",
|
||||||
|
"alt": "Bild aus Originalartikel",
|
||||||
|
"copyright_text": "Unbekannt",
|
||||||
|
"copyright_link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5926"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.camping-in-deutschland.de/camp_bilder/1887.345.thb.jpg",
|
||||||
|
"alt": "Bild aus Originalartikel",
|
||||||
|
"copyright_text": "Unbekannt",
|
||||||
|
"copyright_link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5926"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Artikel 5923 von camping-news.de",
|
||||||
|
"title": "Komfort und Flexibilit\u00e4t f\u00fcr moderne Camper",
|
||||||
|
"date": "Wed, 07 May 2025 00:00:00 +0200",
|
||||||
|
"summary": "dwt-Zelte präsentiert das Luftzelt Scala Air",
|
||||||
|
"text": "Der Hersteller dwt-Zelte pr\u00e4sentiert mit dem Modell Scala Air ein neues Zelt f\u00fcr Camper, das in zwei Gr\u00f6\u00dfen erh\u00e4ltlich ist und sich durch seinen gro\u00dfz\u00fcgigen Raum und eine durchdachte Bel\u00fcftung auszeichnet. Das Zelt nutzt eine moderne Air-In-Technologie f\u00fcr einen schnellen Aufbau und bietet eine stabile Struktur dank robuster Luftschl\u00e4uche.\n\nDie Ausf\u00fchrungen Scala Air 250 und Scala Air 280 unterscheiden sich in ihrer Tiefe von 250 bzw. 280 cm und eignen sich besonders f\u00fcr Kurzurlaube und wechselfrequente Standorte. Die Zelte sind windstabil und dank zus\u00e4tzlicher Luftschl\u00e4uche und durchdachter Fensterkonstruktionen optimal bel\u00fcftet. \n\nDas Design ist modular, was eine flexible Raumgestaltung erm\u00f6glicht. Entnehmbare W\u00e4nde, hochrollbare T\u00fcr und anpassbare Seitenw\u00e4nde bieten ein individuelles Raumkonzept. Das Zelt ist aus robusten, wetterfesten und pflegeleichten Materialien gefertigt und der Preis bewegt sich je nach Gr\u00f6\u00dfe zwischen 1.995 \u20ac und 2.215 \u20ac.\n\nDie Zelte verf\u00fcgen \u00fcber Sicherheitsgurte f\u00fcr windige Standorte und sind mit H\u00f6henausgleichs-Blocks ausgestattet, um Bodenunebenheiten ausgleichen zu k\u00f6nnen. Damit richten sich die Scala Air Zelte an alle Camper, die Wert auf ein flexibles, robustes und schnell aufzubauendes Zelt legen. Weitere Informationen sind auf der Herstellerseite zu finden.",
|
||||||
|
"tags": [
|
||||||
|
"\"Camping\"",
|
||||||
|
"\"dwt-Zelte\"",
|
||||||
|
"\"Air-In-Technologie\""
|
||||||
|
],
|
||||||
|
"status": "Process",
|
||||||
|
"link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5923",
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"url": "https://www.camping-in-deutschland.de/pics2020/logo_weiss.png",
|
||||||
|
"alt": "Bild aus Originalartikel",
|
||||||
|
"copyright_text": "Unbekannt",
|
||||||
|
"copyright_link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5923"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.camping-in-deutschland.de/img/news13/scala_dia5199.jpg",
|
||||||
|
"alt": "Bild aus Originalartikel",
|
||||||
|
"copyright_text": "Unbekannt",
|
||||||
|
"copyright_link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5923"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.camping-in-deutschland.de/pics2013/scrollbox-shadow.png",
|
||||||
|
"alt": "Bild aus Originalartikel",
|
||||||
|
"copyright_text": "Unbekannt",
|
||||||
|
"copyright_link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5923"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.camping-in-deutschland.de/pics2013/scrollbox-shadow.png",
|
||||||
|
"alt": "Bild aus Originalartikel",
|
||||||
|
"copyright_text": "Unbekannt",
|
||||||
|
"copyright_link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5923"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.camping-in-deutschland.de/pics2013/scrollbox-shadow.png",
|
||||||
|
"alt": "Bild aus Originalartikel",
|
||||||
|
"copyright_text": "Unbekannt",
|
||||||
|
"copyright_link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5923"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.camping-in-deutschland.de/pics2013/scrollbox-shadow.png",
|
||||||
|
"alt": "Bild aus Originalartikel",
|
||||||
|
"copyright_text": "Unbekannt",
|
||||||
|
"copyright_link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5923"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.hundefreundliche-campingplaetze.de/logos/Familienfreundliche.png",
|
||||||
|
"alt": "Bild aus Originalartikel",
|
||||||
|
"copyright_text": "Unbekannt",
|
||||||
|
"copyright_link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5923"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.camping-in-deutschland.de/camp_bilder/2940.345.thb.jpg",
|
||||||
|
"alt": "Bild aus Originalartikel",
|
||||||
|
"copyright_text": "Unbekannt",
|
||||||
|
"copyright_link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5923"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.camping-in-deutschland.de/camp_bilder/2433.345.thb.jpg",
|
||||||
|
"alt": "Bild aus Originalartikel",
|
||||||
|
"copyright_text": "Unbekannt",
|
||||||
|
"copyright_link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5923"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.camping-in-deutschland.de/camp_bilder/2495.345.thb.jpg",
|
||||||
|
"alt": "Bild aus Originalartikel",
|
||||||
|
"copyright_text": "Unbekannt",
|
||||||
|
"copyright_link": "https://www.camping-in-deutschland.de/index.php?pid=meldung&id=5923"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -5,3 +5,52 @@ INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1
|
||||||
INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
|
INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
|
||||||
INFO:root:✅ Artikel 'Abenteuer für die Kleinen, Entspannung für
|
INFO:root:✅ Artikel 'Abenteuer für die Kleinen, Entspannung für
|
||||||
die Großen' umgeschrieben.
|
die Großen' umgeschrieben.
|
||||||
|
INFO:root:Abrufen von Feed: https://www.camping-news.de/rss/
|
||||||
|
INFO:root:Starte Umschreiben von Artikeln mit Status 'Rewrite' ...
|
||||||
|
INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
|
||||||
|
INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
|
||||||
|
INFO:root:✅ Artikel 'Den eigenen Verbrauchszahlen auf der Spur' umgeschrieben.
|
||||||
|
INFO:root:Starte Umschreiben von Artikeln mit Status 'Rewrite' ...
|
||||||
|
INFO:root:Abrufen von Feed: https://www.camping-news.de/rss/
|
||||||
|
INFO:root:🔄 Starte Verarbeitung von 0 Feeds...
|
||||||
|
INFO:root:✅ Alle Feeds erfolgreich verarbeitet.
|
||||||
|
INFO:root:🔄 Starte Verarbeitung von 1 Feeds...
|
||||||
|
INFO:root:📥 Verarbeite Feed: https://www.camping-news.de/rss/
|
||||||
|
INFO:root:✅ Alle Feeds erfolgreich verarbeitet.
|
||||||
|
INFO:root:🔄 Starte Verarbeitung von 1 Feeds...
|
||||||
|
INFO:root:📥 Verarbeite Feed: https://www.camping-news.de/rss/
|
||||||
|
INFO:root:✅ Alle Feeds erfolgreich verarbeitet.
|
||||||
|
2025-07-05 15:53:57,795 - INFO - Lade Feed: https://www.camping-news.de/rss/
|
||||||
|
2025-07-05 15:54:02,133 - INFO - 10 neue Artikel gefunden in https://www.camping-news.de/rss/
|
||||||
|
2025-07-05 15:54:02,136 - INFO - 10 neue Artikel gespeichert.
|
||||||
|
2025-07-05 15:57:50,216 - INFO - Lade Feed: https://www.camping-news.de/rss/
|
||||||
|
2025-07-05 15:57:54,463 - INFO - 10 neue Artikel gefunden in https://www.camping-news.de/rss/
|
||||||
|
2025-07-05 15:57:54,465 - INFO - 10 neue Artikel gespeichert.
|
||||||
|
2025-07-05 16:00:32,416 - INFO - Lade Feed: https://www.camping-news.de/rss/
|
||||||
|
2025-07-05 16:00:36,108 - INFO - 9 neue Artikel gefunden in https://www.camping-news.de/rss/
|
||||||
|
2025-07-05 16:00:36,109 - INFO - 9 neue Artikel gespeichert.
|
||||||
|
2025-07-05 16:06:28,832 - INFO - Lade Feed: https://www.camping-news.de/rss/
|
||||||
|
2025-07-05 16:06:30,875 - INFO - 4 neue Artikel gefunden in https://www.camping-news.de/rss/
|
||||||
|
2025-07-05 16:06:30,877 - INFO - 4 neue Artikel gespeichert.
|
||||||
|
2025-07-05 16:08:14,063 - INFO - Lade Feed: https://www.camping-news.de/rss/
|
||||||
|
2025-07-05 16:08:15,413 - INFO - 3 neue Artikel gefunden in https://www.camping-news.de/rss/
|
||||||
|
2025-07-05 16:08:15,414 - INFO - 3 neue Artikel gespeichert.
|
||||||
|
2025-07-05 16:09:14,943 - INFO - Lade Feed: https://www.camping-news.de/rss/
|
||||||
|
2025-07-05 16:09:18,883 - INFO - 9 neue Artikel gefunden in https://www.camping-news.de/rss/
|
||||||
|
2025-07-05 16:09:18,885 - INFO - 9 neue Artikel gespeichert.
|
||||||
|
2025-07-05 16:17:36,273 - INFO - Lade Feed: https://www.camping-news.de/rss/
|
||||||
|
2025-07-05 16:17:36,691 - INFO - 0 neue Artikel gefunden in https://www.camping-news.de/rss/
|
||||||
|
2025-07-05 16:17:36,691 - INFO - Keine neuen Artikel gefunden.
|
||||||
|
2025-07-05 16:18:45,562 - INFO - ✍️ Umschreiben von: Den eigenen Verbrauchszahlen auf der Spur
|
||||||
|
2025-07-05 16:19:04,394 - INFO - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
|
||||||
|
2025-07-05 16:19:06,129 - INFO - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
|
||||||
|
2025-07-05 16:19:06,132 - INFO - ✅ Artikel umgeschrieben: Den eigenen Verbrauchszahlen auf der Spur
|
||||||
|
2025-07-05 16:19:06,132 - INFO - ✍️ Umschreiben von: Das weltweit größte Caravaning-Erlebnis
|
||||||
|
2025-07-05 16:19:18,595 - INFO - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
|
||||||
|
2025-07-05 16:19:20,334 - INFO - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
|
||||||
|
2025-07-05 16:19:20,336 - INFO - ✅ Artikel umgeschrieben: Das weltweit größte Caravaning-Erlebnis
|
||||||
|
2025-07-05 16:19:20,336 - INFO - ✍️ Umschreiben von: Komfort und Flexibilität für moderne Camper
|
||||||
|
2025-07-05 16:19:33,242 - INFO - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
|
||||||
|
2025-07-05 16:19:34,796 - INFO - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
|
||||||
|
2025-07-05 16:19:34,799 - INFO - ✅ Artikel umgeschrieben: Komfort und Flexibilität für moderne Camper
|
||||||
|
2025-07-05 16:19:34,803 - INFO - Alle Artikel mit Status 'Rewrite' wurden verarbeitet.
|
||||||
|
|
|
||||||
219
main.py
219
main.py
|
|
@ -1,108 +1,169 @@
|
||||||
# main.py
|
# main.py
|
||||||
|
|
||||||
|
import feedparser
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import feedparser
|
|
||||||
from utils.image_extractor import extract_images_with_metadata
|
|
||||||
from openai import OpenAI
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import logging
|
import logging
|
||||||
|
from utils.image_extractor import extract_images_with_metadata
|
||||||
|
import openai
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
# Log-Verzeichnis sicherstellen
|
# Logging konfigurieren
|
||||||
os.makedirs("logs", exist_ok=True)
|
log_dir = "logs"
|
||||||
|
os.makedirs(log_dir, exist_ok=True)
|
||||||
|
logging.basicConfig(
|
||||||
|
filename=os.path.join(log_dir, "rss_tool.log"),
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s - %(levelname)s - %(message)s"
|
||||||
|
)
|
||||||
|
|
||||||
|
openai.api_key = os.getenv("OPENAI_API_KEY")
|
||||||
|
|
||||||
client = OpenAI()
|
ARTICLES_FILE = "data/articles.json"
|
||||||
# 📝 Logging konfigurieren
|
FEEDS_FILE = "data/feeds.json"
|
||||||
logging.basicConfig(filename='logs/rss_tool.log', level=logging.INFO)
|
|
||||||
|
|
||||||
ARTICLES_FILE = "processed_articles.json"
|
VALID_STATUSES = ["New", "Rewrite", "Process", "Online", "On Hold", "Trash"]
|
||||||
FEEDS_FILE = "feeds.json"
|
|
||||||
|
|
||||||
def load_articles():
|
|
||||||
if os.path.exists(ARTICLES_FILE):
|
|
||||||
with open(ARTICLES_FILE, "r") as f:
|
|
||||||
return json.load(f)
|
|
||||||
return []
|
|
||||||
|
|
||||||
def save_articles(articles):
|
|
||||||
with open(ARTICLES_FILE, "w") as f:
|
|
||||||
json.dump(articles, f, indent=2)
|
|
||||||
|
|
||||||
def load_feeds():
|
def load_feeds():
|
||||||
if os.path.exists(FEEDS_FILE):
|
if not os.path.exists(FEEDS_FILE):
|
||||||
with open(FEEDS_FILE, "r") as f:
|
return []
|
||||||
return json.load(f)
|
with open(FEEDS_FILE, "r") as f:
|
||||||
return []
|
return json.load(f)
|
||||||
|
|
||||||
def save_feeds(feeds):
|
def save_feeds(feeds):
|
||||||
with open(FEEDS_FILE, "w") as f:
|
with open(FEEDS_FILE, "w") as f:
|
||||||
json.dump(feeds, f, indent=2)
|
json.dump(feeds, f, indent=2)
|
||||||
|
|
||||||
def fetch_and_process_feed(url):
|
def load_articles():
|
||||||
logging.info(f"Abrufen von Feed: {url}")
|
if not os.path.exists(ARTICLES_FILE):
|
||||||
feed = feedparser.parse(url)
|
return []
|
||||||
articles = load_articles()
|
with open(ARTICLES_FILE, "r") as f:
|
||||||
for entry in feed.entries:
|
articles = json.load(f)
|
||||||
if any(a["link"] == entry.link for a in articles):
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
images = extract_images_with_metadata(entry.link)
|
|
||||||
except Exception as e:
|
|
||||||
logging.warning(f"Fehler beim Bildextrakt: {e}")
|
|
||||||
images = []
|
|
||||||
article = {
|
|
||||||
"id": f"{entry.link}",
|
|
||||||
"title": entry.title,
|
|
||||||
"summary": entry.summary,
|
|
||||||
"link": entry.link,
|
|
||||||
"date": entry.get("published", datetime.now().isoformat()),
|
|
||||||
"text": entry.summary,
|
|
||||||
"status": "New",
|
|
||||||
"images": images,
|
|
||||||
"tags": []
|
|
||||||
}
|
|
||||||
articles.append(article)
|
|
||||||
save_articles(articles)
|
|
||||||
|
|
||||||
def process_articles():
|
# Sicherstellen, dass jeder Artikel einen gültigen Status hat
|
||||||
|
for article in articles:
|
||||||
|
if article.get("status") not in VALID_STATUSES:
|
||||||
|
article["status"] = "New"
|
||||||
|
return articles
|
||||||
|
|
||||||
|
def save_articles(articles):
|
||||||
|
with open(ARTICLES_FILE, "w") as f:
|
||||||
|
json.dump(articles, f, indent=2)
|
||||||
|
|
||||||
|
def fetch_and_process_feed(feed_url, existing_ids):
|
||||||
|
feed = feedparser.parse(feed_url)
|
||||||
|
new_articles = []
|
||||||
|
|
||||||
|
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", "")
|
||||||
|
|
||||||
|
soup = BeautifulSoup(content, "html.parser")
|
||||||
|
clean_text = soup.get_text(" ", strip=True)
|
||||||
|
|
||||||
|
images = extract_images_with_metadata(entry.link)
|
||||||
|
|
||||||
|
new_articles.append({
|
||||||
|
"id": article_id,
|
||||||
|
"title": title,
|
||||||
|
"date": date,
|
||||||
|
"summary": summary,
|
||||||
|
"text": clean_text,
|
||||||
|
"tags": [],
|
||||||
|
"status": "New",
|
||||||
|
"link": entry.get("link", ""),
|
||||||
|
"images": images
|
||||||
|
})
|
||||||
|
|
||||||
|
return new_articles
|
||||||
|
|
||||||
|
def process_articles(existing_ids):
|
||||||
feeds = load_feeds()
|
feeds = load_feeds()
|
||||||
for url in feeds:
|
all_articles = load_articles()
|
||||||
fetch_and_process_feed(url)
|
new_entries = []
|
||||||
|
|
||||||
|
for feed in feeds:
|
||||||
|
if isinstance(feed, dict):
|
||||||
|
url = feed.get("url")
|
||||||
|
else:
|
||||||
|
url = feed
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
logging.info(f"Lade Feed: {url}")
|
||||||
|
entries = fetch_and_process_feed(url, existing_ids)
|
||||||
|
new_entries.extend(entries)
|
||||||
|
logging.info(f"{len(entries)} neue Artikel gefunden in {url}")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Fehler beim Verarbeiten von {url}: {e}")
|
||||||
|
|
||||||
|
# Nur neue Artikel speichern, deren ID noch nicht vorhanden ist
|
||||||
|
existing_article_ids = set(article["id"] for article in all_articles)
|
||||||
|
unique_new_entries = [a for a in new_entries if a["id"] not in existing_article_ids]
|
||||||
|
|
||||||
|
if unique_new_entries:
|
||||||
|
all_articles.extend(unique_new_entries)
|
||||||
|
save_articles(all_articles)
|
||||||
|
logging.info(f"{len(unique_new_entries)} neue Artikel gespeichert.")
|
||||||
|
else:
|
||||||
|
logging.info("Keine neuen Artikel gefunden.")
|
||||||
|
|
||||||
def rewrite_articles():
|
def rewrite_articles():
|
||||||
logging.info("Starte Umschreiben von Artikeln mit Status 'Rewrite' ...")
|
|
||||||
articles = load_articles()
|
articles = load_articles()
|
||||||
updated = False
|
|
||||||
for article in articles:
|
for article in articles:
|
||||||
if article["status"] != "Rewrite":
|
if article.get("status") == "Rewrite":
|
||||||
continue
|
try:
|
||||||
try:
|
logging.info(f"✍️ Umschreiben von: {article['title']}")
|
||||||
prompt = f"Fasse diesen Text neu und interessant zusammen:\n\n{article['summary']}"
|
prompt = f"Schreibe folgenden Artikel um und fasse ihn verständlich zusammen:\n\n{article['text']}"
|
||||||
response = client.chat.completions.create(
|
response = openai.chat.completions.create(
|
||||||
model="gpt-4",
|
model="gpt-4",
|
||||||
messages=[{"role": "user", "content": prompt}]
|
messages=[
|
||||||
)
|
{"role": "system", "content": "Du bist ein professioneller Redakteur."},
|
||||||
rewritten = response.choices[0].message.content.strip()
|
{"role": "user", "content": prompt}
|
||||||
article["text"] = rewritten
|
]
|
||||||
article["status"] = "Done"
|
)
|
||||||
|
new_text = response.choices[0].message.content.strip()
|
||||||
|
article["text"] = f"{article['title']}\n\n{new_text}"
|
||||||
|
article["status"] = "Process"
|
||||||
|
|
||||||
# Tags generieren
|
tag_prompt = f"Erstelle 3 passende, kurze Stichwörter (Tags) für diesen Artikel:\n\n{new_text}"
|
||||||
tag_prompt = f"Erstelle passende 3-5 Tags für diesen Text:\n\n{rewritten}"
|
tag_response = openai.chat.completions.create(
|
||||||
tag_response = client.chat.completions.create(
|
model="gpt-4",
|
||||||
model="gpt-4",
|
messages=[
|
||||||
messages=[{"role": "user", "content": tag_prompt}]
|
{"role": "system", "content": "Du bist ein Blog-Tag-Generator."},
|
||||||
)
|
{"role": "user", "content": tag_prompt}
|
||||||
tags = [tag.strip() for tag in tag_response.choices[0].message.content.split(",")]
|
]
|
||||||
article["tags"] = tags
|
)
|
||||||
|
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
|
||||||
|
|
||||||
updated = True
|
# Sicherstellen, dass Bildmetadaten vollständig sind
|
||||||
logging.info(f"✅ Artikel '{article['title']}' umgeschrieben.")
|
for img in article.get("images", []):
|
||||||
except Exception as e:
|
if "caption" not in img:
|
||||||
logging.error(f"❌ Fehler beim Umschreiben von '{article['title']}':\n{e}")
|
img["caption"] = "Kein Bildtitel vorhanden"
|
||||||
if updated:
|
if "copyright" not in img:
|
||||||
save_articles(articles)
|
img["copyright"] = "Unbekannt"
|
||||||
|
if "copyright_url" not in img:
|
||||||
|
img["copyright_url"] = "#"
|
||||||
|
|
||||||
|
logging.info(f"✅ Artikel umgeschrieben: {article['title']}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"❌ Fehler beim Umschreiben von '{article['title']}': {e}")
|
||||||
|
|
||||||
|
save_articles(articles)
|
||||||
|
logging.info("Alle Artikel mit Status 'Rewrite' wurden verarbeitet.")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue