From 2db9544fda54f4cc3b1579ff953d921214b1ee0c Mon Sep 17 00:00:00 2001 From: Oliver G Date: Sat, 5 Jul 2025 16:36:29 +0200 Subject: [PATCH] UI-Anpassung: Feed-Anzeige entfernt, Tabelle verbessert --- .gitignore | 4 +- CHANGELOG.md | 20 +++ app.log | 5 - app.py | 143 +++++++-------- data/articles.json | 154 ++++++++++++++++ feeds.json => data/feeds.json | 0 logs/rss_tool.log | 49 +++++ main.py | 219 +++++++++++++++-------- articles.json => processed_articles.json | 0 9 files changed, 438 insertions(+), 156 deletions(-) delete mode 100644 app.log create mode 100644 data/articles.json rename feeds.json => data/feeds.json (100%) rename articles.json => processed_articles.json (100%) diff --git a/.gitignore b/.gitignore index 50bc5d6..eb1ffb8 100644 --- a/.gitignore +++ b/.gitignore @@ -24,8 +24,8 @@ Thumbs.db .env # JSON-Datenbanken / Zwischenspeicherung -*.json -!feeds.json # optional entfernbar, wenn du Beispielfeeds mit versionieren willst + *.json + !feeds.json # optional entfernbar, wenn du Beispielfeeds mit versionieren willst # LOG Files ausschließen, dienen nur zum Debuggen # *.log diff --git a/CHANGELOG.md b/CHANGELOG.md index fc34aec..a671d75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # 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 ### Hinzugefügt - Automatische Bilderkennung beim Einlesen von Artikeln diff --git a/app.log b/app.log deleted file mode 100644 index 71c69fb..0000000 --- a/app.log +++ /dev/null @@ -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. diff --git a/app.py b/app.py index 40ec91e..0c15e7a 100644 --- a/app.py +++ b/app.py @@ -1,92 +1,95 @@ # app.py import streamlit as st -import json -import os from datetime import datetime -from dotenv import load_dotenv -from email.utils import parsedate_to_datetime -from main import load_articles, save_articles, process_articles, fetch_and_process_feed, rewrite_articles +from main import ( + load_feeds, + 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 -status_filter = st.sidebar.selectbox("🔍 Artikelstatus filtern", ["Alle", "New", "Rewrite", "Process", "Online", "On Hold", "Trash"]) - -# Neuen Feed hinzufügen -st.sidebar.markdown("---") -st.sidebar.header("➕ RSS Feed hinzufügen") -new_feed_url = st.sidebar.text_input("Feed URL") -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() +#if feeds: +# st.sidebar.write("### Aktuelle Feeds:") +# for feed in feeds: +# url = feed["url"] if isinstance(feed, dict) else feed +# st.sidebar.markdown(f"- {url}") +#else: +# st.sidebar.info("Noch keine Feeds hinzugefügt.") # Artikel laden -try: - articles = load_articles() -except json.decoder.JSONDecodeError: - articles = [] +if st.sidebar.button("🔄 Alle Feeds neu laden"): + existing_ids = [a["id"] for a in load_articles()] + process_articles(existing_ids) + 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": articles = [a for a in articles if a.get("status") == status_filter] -# Artikelübersicht -st.title("📰 RSS Artikel Übersicht") -st.markdown("---") - -if not articles: - st.info("Keine Artikel gefunden.") -else: - st.markdown("### 📄 Artikelliste") - selected_ids = [] - all_statuses = ["New", "Rewrite", "Process", "Online", "On Hold", "Trash"] +# Tabelle anzeigen +if articles: + st.markdown("### 📄 Übersichtstabelle") + st.write("**Spaltenübersicht:** Auswahl | Datum | Titel | Zusammenfassung | Wörter | Tags | Status") for article in articles: - col1, col2, col3, col4, col5, col6, col7 = st.columns([0.5, 1.2, 2.5, 2, 1, 2, 1.2]) - with col1: - if st.checkbox("", key=f"select_{article['id']}"): - selected_ids.append(article['id']) - with col2: - date = parsedate_to_datetime(article['date']).strftime("%d.%m.%y") - st.markdown(date) - with col3: - st.markdown(article['title']) - with col4: - st.markdown(article['summary'][:150] + ("..." if len(article['summary']) > 150 else "")) - with col5: - word_count = len(article['text'].split()) - st.markdown(str(word_count)) - with col6: + cols = st.columns([0.05, 0.1, 0.2, 0.25, 0.05, 0.2, 0.15]) + with cols[0]: + st.checkbox("", key=f"select_{article['id']}") + with cols[1]: + 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]) + with cols[2]: + st.markdown(f"**{article['title']}**") + with cols[3]: + st.markdown(article.get("summary", "")[:150]) + with cols[4]: + st.markdown(str(len(article.get("text", "").split()))) + with cols[5]: st.markdown(", ".join(article.get("tags", []))) - with col7: - status = st.selectbox("", all_statuses, index=all_statuses.index(article.get("status", "New")), key=f"status_{article['id']}") - if status != article.get("status"): - article["status"] = status + with cols[6]: + status_options = ["New", "Rewrite", "Process", "Online", "On Hold", "Trash"] + current_status = article.get("status", "New") + 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) st.rerun() - if selected_ids: - new_status = st.selectbox("Status für ausgewählte Artikel setzen", all_statuses) - if st.button("✅ Status aktualisieren"): - for article in articles: - if article['id'] in selected_ids: - article['status'] = new_status - save_articles(articles) - st.success("Status aktualisiert.") - st.rerun() + with st.expander(f"🔍 {article['title']}"): + st.markdown("#### ✍️ Artikeltext") + st.code(f"{article['title']}\n\n{article['text']}\n\nQuelle: {article['link']}", language="markdown") - st.markdown("---") + st.markdown("#### 🏷️ Tags") + st.code(", ".join(article.get("tags", [])), language="markdown") - if st.button("✍️ Artikel mit Status 'Rewrite' umschreiben"): - rewrite_articles() - st.rerun() + st.markdown("#### 🖼️ Bilder") + for img in article.get("images", []): + 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("---") \ No newline at end of file +else: + st.info("Keine Artikel für den gewählten Status gefunden.") diff --git a/data/articles.json b/data/articles.json new file mode 100644 index 0000000..5a34004 --- /dev/null +++ b/data/articles.json @@ -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" + } + ] + } +] \ No newline at end of file diff --git a/feeds.json b/data/feeds.json similarity index 100% rename from feeds.json rename to data/feeds.json diff --git a/logs/rss_tool.log b/logs/rss_tool.log index f56cae2..1e73725 100644 --- a/logs/rss_tool.log +++ b/logs/rss_tool.log @@ -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:root:✅ Artikel 'Abenteuer für die Kleinen, Entspannung für 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. diff --git a/main.py b/main.py index 42d4072..3378318 100644 --- a/main.py +++ b/main.py @@ -1,108 +1,169 @@ # main.py +import feedparser import json import os +from bs4 import BeautifulSoup from datetime import datetime -import feedparser -from utils.image_extractor import extract_images_with_metadata -from openai import OpenAI from dotenv import load_dotenv import logging - +from utils.image_extractor import extract_images_with_metadata +import openai load_dotenv() -# Log-Verzeichnis sicherstellen -os.makedirs("logs", exist_ok=True) +# Logging konfigurieren +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() -# 📝 Logging konfigurieren -logging.basicConfig(filename='logs/rss_tool.log', level=logging.INFO) +ARTICLES_FILE = "data/articles.json" +FEEDS_FILE = "data/feeds.json" -ARTICLES_FILE = "processed_articles.json" -FEEDS_FILE = "feeds.json" +VALID_STATUSES = ["New", "Rewrite", "Process", "Online", "On Hold", "Trash"] -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(): - if os.path.exists(FEEDS_FILE): - with open(FEEDS_FILE, "r") as f: - return json.load(f) - return [] + if not os.path.exists(FEEDS_FILE): + return [] + with open(FEEDS_FILE, "r") as f: + return json.load(f) def save_feeds(feeds): with open(FEEDS_FILE, "w") as f: json.dump(feeds, f, indent=2) -def fetch_and_process_feed(url): - logging.info(f"Abrufen von Feed: {url}") - feed = feedparser.parse(url) - articles = load_articles() - for entry in feed.entries: - 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 load_articles(): + if not os.path.exists(ARTICLES_FILE): + return [] + with open(ARTICLES_FILE, "r") as f: + articles = json.load(f) -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() - for url in feeds: - fetch_and_process_feed(url) + all_articles = load_articles() + 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(): - logging.info("Starte Umschreiben von Artikeln mit Status 'Rewrite' ...") articles = load_articles() - updated = False for article in articles: - if article["status"] != "Rewrite": - continue - try: - prompt = f"Fasse diesen Text neu und interessant zusammen:\n\n{article['summary']}" - response = client.chat.completions.create( - model="gpt-4", - messages=[{"role": "user", "content": prompt}] - ) - rewritten = response.choices[0].message.content.strip() - article["text"] = rewritten - article["status"] = "Done" + if article.get("status") == "Rewrite": + try: + logging.info(f"✍️ Umschreiben von: {article['title']}") + prompt = f"Schreibe folgenden Artikel um und fasse ihn verständlich zusammen:\n\n{article['text']}" + response = openai.chat.completions.create( + model="gpt-4", + messages=[ + {"role": "system", "content": "Du bist ein professioneller Redakteur."}, + {"role": "user", "content": prompt} + ] + ) + 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 passende 3-5 Tags für diesen Text:\n\n{rewritten}" - tag_response = client.chat.completions.create( - model="gpt-4", - messages=[{"role": "user", "content": tag_prompt}] - ) - tags = [tag.strip() for tag in tag_response.choices[0].message.content.split(",")] - article["tags"] = tags + tag_prompt = f"Erstelle 3 passende, kurze Stichwörter (Tags) für diesen Artikel:\n\n{new_text}" + tag_response = openai.chat.completions.create( + model="gpt-4", + messages=[ + {"role": "system", "content": "Du bist ein Blog-Tag-Generator."}, + {"role": "user", "content": tag_prompt} + ] + ) + 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 - logging.info(f"✅ Artikel '{article['title']}' umgeschrieben.") - except Exception as e: - logging.error(f"❌ Fehler beim Umschreiben von '{article['title']}':\n{e}") - if updated: - save_articles(articles) + # Sicherstellen, dass Bildmetadaten vollständig sind + for img in article.get("images", []): + if "caption" not in img: + img["caption"] = "Kein Bildtitel vorhanden" + if "copyright" not in img: + 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.") diff --git a/articles.json b/processed_articles.json similarity index 100% rename from articles.json rename to processed_articles.json