rss-news/app.py

1003 lines
No EOL
39 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# app.py
import streamlit as st
from datetime import datetime
from main import (
load_feeds,
save_feeds,
load_articles,
save_articles,
process_articles,
rewrite_articles,
upload_articles_to_wp
)
from utils.dalle_generator import generate_dalle_image
from utils.wordpress_uploader import WordPressUploader
import os
from collections import Counter
import time
# === Page Configuration ===
st.set_page_config(
page_title="📰 RSS Artikel Manager",
layout="wide",
initial_sidebar_state="collapsed"
)
# === Custom CSS für modernes Design ===
st.markdown("""
<style>
/* Hauptcontainer */
.main-header {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
padding: 2rem;
border-radius: 10px;
margin-bottom: 2rem;
color: white;
text-align: center;
}
/* Artikel Cards */
.article-card {
background: white;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border-left: 4px solid #667eea;
transition: transform 0.2s;
}
.article-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15);
}
/* Status Badges */
.status-badge {
padding: 0.3rem 0.8rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: bold;
margin-right: 0.5rem;
}
.status-new { background-color: #e3f2fd; color: #1976d2; }
.status-rewrite { background-color: #fff3e0; color: #f57c00; }
.status-process { background-color: #f3e5f5; color: #7b1fa2; }
.status-online { background-color: #e8f5e8; color: #388e3c; }
.status-hold { background-color: #fce4ec; color: #c2185b; }
.status-trash { background-color: #ffebee; color: #d32f2f; }
.status-wp-pending { background-color: #e1f5fe; color: #0277bd; }
/* Filter Section */
.filter-section {
background: #f8f9fa;
padding: 1.5rem;
border-radius: 10px;
margin-bottom: 2rem;
}
/* Stats Cards */
.stats-card {
background: white;
padding: 1.5rem;
border-radius: 10px;
text-align: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.stats-number {
font-size: 2rem;
font-weight: bold;
color: #667eea;
}
/* Action Buttons */
.action-button {
margin: 0.25rem;
}
/* Image Gallery */
.image-gallery {
display: flex;
gap: 1rem;
overflow-x: auto;
padding: 1rem 0;
}
.image-item {
min-width: 200px;
text-align: center;
}
/* WordPress Upload Status */
.wp-status {
background: #e3f2fd;
padding: 1rem;
border-radius: 8px;
margin: 1rem 0;
border-left: 4px solid #2196f3;
}
</style>
""", unsafe_allow_html=True)
# === Initialize Session State ===
if 'selected_articles' not in st.session_state:
st.session_state.selected_articles = set()
if 'search_query' not in st.session_state:
st.session_state.search_query = ""
if 'status_filter' not in st.session_state:
st.session_state.status_filter = "New"
if 'feed_filter' not in st.session_state:
st.session_state.feed_filter = "Alle"
# === Helper Functions ===
def get_status_badge(status):
"""Erstellt einen farbigen Status-Badge"""
status_classes = {
"New": "status-new",
"Rewrite": "status-rewrite",
"Process": "status-process",
"Online": "status-online",
"On Hold": "status-hold",
"Trash": "status-trash",
"WordPress Pending": "status-wp-pending"
}
class_name = status_classes.get(status, "status-new")
return f'<span class="status-badge {class_name}">{status}</span>'
def format_date(date_str):
"""Formatiert Datum für bessere Lesbarkeit"""
try:
if "GMT" in date_str or "+" in date_str:
return datetime.strptime(date_str, "%a, %d %b %Y %H:%M:%S %z").strftime("%d.%m.%Y %H:%M")
else:
return date_str[:16].replace("T", " ")
except:
return date_str[:10]
def get_word_count(text):
"""Zählt Wörter im Text"""
return len(text.split()) if text else 0
def show_notification(message, type="success"):
"""Zeigt eine 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 test_wordpress_connection():
"""Testet die WordPress-Verbindung"""
try:
uploader = WordPressUploader()
success, message = uploader.test_connection()
return success, message
except Exception as e:
return False, f"Fehler beim Testen der Verbindung: {str(e)}"
# === Header ===
st.markdown("""
<div class="main-header">
<h1>📰 RSS Artikel Manager</h1>
<p>Moderne Verwaltung deiner RSS-Feeds und Artikel mit WordPress-Integration</p>
</div>
""", unsafe_allow_html=True)
# === Tab Navigation ===
tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs([
"📋 Dashboard",
"📰 Artikel",
"📡 Feeds",
"🖼️ Bilder",
"📊 Statistiken",
"🔧 WordPress"
])
# === Dashboard Tab ===
with tab1:
st.header("📊 Übersicht")
# Lade Daten
all_articles = load_articles()
feeds = load_feeds()
# Statistiken
col1, col2, col3, col4, col5 = st.columns(5)
with col1:
st.markdown("""
<div class="stats-card">
<div class="stats-number">{}</div>
<div>Gesamt Artikel</div>
</div>
""".format(len(all_articles)), unsafe_allow_html=True)
with col2:
new_count = len([a for a in all_articles if a.get("status") == "New"])
st.markdown("""
<div class="stats-card">
<div class="stats-number">{}</div>
<div>Neue Artikel</div>
</div>
""".format(new_count), unsafe_allow_html=True)
with col3:
process_count = len([a for a in all_articles if a.get("status") == "Process"])
st.markdown("""
<div class="stats-card">
<div class="stats-number">{}</div>
<div>Bereit für WP</div>
</div>
""".format(process_count), unsafe_allow_html=True)
with col4:
wp_pending_count = len([a for a in all_articles if a.get("status") == "WordPress Pending"])
st.markdown("""
<div class="stats-card">
<div class="stats-number">{}</div>
<div>WP Ausstehend</div>
</div>
""".format(wp_pending_count), unsafe_allow_html=True)
with col5:
online_count = len([a for a in all_articles if a.get("status") == "Online"])
st.markdown("""
<div class="stats-card">
<div class="stats-number">{}</div>
<div>Online</div>
</div>
""".format(online_count), unsafe_allow_html=True)
st.markdown("<br>", unsafe_allow_html=True)
# Quick Actions
st.subheader("⚡ Schnellaktionen")
col1, col2, col3, col4 = st.columns(4)
with col1:
if st.button("🔄 Alle Feeds aktualisieren", use_container_width=True):
with st.spinner("Feeds werden aktualisiert..."):
existing_ids = [a["id"] for a in all_articles]
process_articles(existing_ids)
show_notification("Feeds erfolgreich aktualisiert!")
time.sleep(1)
st.rerun()
with col2:
if st.button("✍️ Artikel umschreiben", use_container_width=True):
rewrite_count = len([a for a in all_articles if a.get("status") == "Rewrite"])
if rewrite_count > 0:
with st.spinner(f"{rewrite_count} Artikel werden umgeschrieben..."):
rewrite_articles()
show_notification(f"{rewrite_count} Artikel erfolgreich umgeschrieben!")
time.sleep(1)
st.rerun()
else:
show_notification("Keine Artikel zum Umschreiben gefunden.", "info")
with col3:
if st.button("📤 WordPress Upload", use_container_width=True):
process_count = len([a for a in all_articles if a.get("status") == "Process"])
if process_count > 0:
with st.spinner(f"{process_count} Artikel werden zu WordPress hochgeladen..."):
upload_results = upload_articles_to_wp()
if upload_results.get('error'):
show_notification(f"Fehler beim WordPress-Upload: {upload_results['error']}", "error")
else:
successful = upload_results.get('successful', 0)
failed = upload_results.get('failed', 0)
duplicates = upload_results.get('duplicates', 0)
if successful > 0:
show_notification(f"{successful} Artikel erfolgreich zu WordPress hochgeladen!")
if failed > 0:
show_notification(f"⚠️ {failed} Artikel konnten nicht hochgeladen werden.", "warning")
if duplicates > 0:
show_notification(f" {duplicates} Duplikate übersprungen.", "info")
time.sleep(2)
st.rerun()
else:
show_notification("Keine Artikel für WordPress-Upload gefunden.", "info")
with col4:
if st.button("🧹 Aufräumen", use_container_width=True):
trash_count = len([a for a in all_articles if a.get("status") == "Trash"])
if trash_count > 0:
show_notification(f"{trash_count} Artikel im Papierkorb gefunden.", "info")
else:
show_notification("Keine Artikel zum Aufräumen gefunden.", "info")
# WordPress-Status-Übersicht
if wp_pending_count > 0 or online_count > 0:
st.subheader("🔗 WordPress-Status")
wp_articles = [a for a in all_articles if a.get("status") in ["WordPress Pending", "Online"]]
for article in wp_articles[:5]: # Nur die ersten 5 anzeigen
st.markdown(f"""
<div class="wp-status">
<strong>{article.get('title', 'Kein Titel')}</strong>
{get_status_badge(article.get('status', 'Unknown'))}
<br>
<small>WP Post ID: {article.get('wp_post_id', 'Unbekannt')} | Upload: {format_date(article.get('wp_upload_date', ''))}</small>
</div>
""", unsafe_allow_html=True)
# Neueste Artikel Preview
st.subheader("🕒 Neueste Artikel")
recent_articles = sorted(all_articles, key=lambda x: x.get("date", ""), reverse=True)[:5]
for article in recent_articles:
st.markdown(f"""
<div class="article-card">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<strong>{article.get('title', 'Kein Titel')}</strong>
<br>
<small>{format_date(article.get('date', ''))}</small>
</div>
<div>
{get_status_badge(article.get('status', 'New'))}
</div>
</div>
</div>
""", unsafe_allow_html=True)
# === Artikel Tab ===
with tab2:
st.header("📰 Artikel verwalten")
# Filter Section
st.markdown('<div class="filter-section">', unsafe_allow_html=True)
st.subheader("🔍 Filter & Suche")
col1, col2, col3 = st.columns(3)
with col1:
status_options = ["Alle", "New", "Rewrite", "Process", "Online", "On Hold", "Trash", "WordPress Pending"]
st.session_state.status_filter = st.selectbox(
"Status",
status_options,
index=status_options.index(st.session_state.status_filter)
)
with col2:
# Feed Filter
source_to_name = {f.get("url"): f.get("name", "Unbekannt") for f in feeds}
source_counter = Counter([a.get("source", "Unbekannt") for a in all_articles])
feed_options = ["Alle"]
feed_map = {"Alle": None}
for source, count in source_counter.items():
name = source_to_name.get(source, "Unbekannt")
label = f"{name} ({count})"
feed_options.append(label)
feed_map[label] = source
selected_feed_label = st.selectbox("Feed", feed_options)
st.session_state.feed_filter = selected_feed_label
with col3:
st.session_state.search_query = st.text_input(
"Suche",
value=st.session_state.search_query,
placeholder="Titel, Text oder Tags durchsuchen..."
)
st.markdown('</div>', unsafe_allow_html=True)
# Filter anwenden
filtered_articles = all_articles
# Status Filter
if st.session_state.status_filter != "Alle":
filtered_articles = [a for a in filtered_articles if a.get("status") == st.session_state.status_filter]
# Feed Filter
if st.session_state.feed_filter != "Alle":
selected_source = feed_map[st.session_state.feed_filter]
filtered_articles = [a for a in filtered_articles if a.get("source") == selected_source]
# Suche
if st.session_state.search_query:
query = st.session_state.search_query.lower()
filtered_articles = [
a for a in filtered_articles
if query in a.get("title", "").lower()
or query in a.get("text", "").lower()
or any(query in tag.lower() for tag in a.get("tags", []))
]
# Ergebnisse anzeigen
st.write(f"**{len(filtered_articles)} Artikel gefunden**")
# Artikel Cards
for article in filtered_articles:
has_incomplete_images = any(
not all(k in img and img[k] for k in ("caption", "copyright", "copyright_url"))
for img in article.get("images", [])
)
# Article Card
st.markdown('<div class="article-card">', unsafe_allow_html=True)
# Header
col1, col2 = st.columns([3, 1])
with col1:
title = article.get("title", "Kein Titel")
if has_incomplete_images:
title += " ⚠️"
st.markdown(f"**{title}**")
st.markdown(f"📅 {format_date(article.get('date', ''))}")
# WordPress-Info anzeigen falls vorhanden
if article.get("wp_post_id"):
st.markdown(f"🔗 WordPress ID: {article.get('wp_post_id')} | Upload: {format_date(article.get('wp_upload_date', ''))}")
with col2:
st.markdown(get_status_badge(article.get("status", "New")), unsafe_allow_html=True)
# Content Preview
summary = article.get("summary", "")[:200]
if len(summary) == 200:
summary += "..."
st.markdown(summary)
# Meta Info
col1, col2, col3 = st.columns(3)
with col1:
st.markdown(f"📝 **{get_word_count(article.get('text', ''))} Wörter**")
with col2:
tags = article.get("tags", [])
if tags:
st.markdown(f"🏷️ {', '.join(tags[:3])}{'...' if len(tags) > 3 else ''}")
with col3:
source_name = source_to_name.get(article.get("source", ""), "Unbekannt")
st.markdown(f"📡 {source_name}")
# Actions
col1, col2, col3, col4, col5 = st.columns(5)
with col1:
# Status ändern
status_options = ["New", "Rewrite", "Process", "Online", "On Hold", "Trash", "WordPress Pending"]
current_status = article.get("status", "New")
new_status = st.selectbox(
"Status",
status_options,
index=status_options.index(current_status),
key=f"status_{article['id']}"
)
if new_status != current_status:
# Artikel in der Liste finden und aktualisieren
for idx, art in enumerate(all_articles):
if art["id"] == article["id"]:
all_articles[idx]["status"] = new_status
break
save_articles(all_articles)
show_notification(f"Status auf '{new_status}' geändert!")
time.sleep(0.5)
st.rerun()
with col2:
if st.button("📋 Text kopieren", key=f"copy_{article['id']}"):
text_to_copy = f"{article['title']}\n\n{article['text']}\n\nQuelle: {article['link']}"
st.code(text_to_copy, language="markdown")
show_notification("Text bereit zum Kopieren!")
with col3:
if st.button("🔗 Original öffnen", key=f"link_{article['id']}"):
st.markdown(f"[🔗 Artikel öffnen]({article.get('link', '#')})")
with col4:
# WordPress Upload Button für einzelne Artikel
if article.get("status") == "Process":
if st.button("📤 WordPress", key=f"wp_upload_{article['id']}"):
with st.spinner("Lade zu WordPress hoch..."):
from utils.wordpress_uploader import upload_single_article_to_wordpress
success, message, wp_post_id = upload_single_article_to_wordpress(article)
if success:
# Status ändern
for idx, art in enumerate(all_articles):
if art["id"] == article["id"]:
all_articles[idx]["status"] = "WordPress Pending"
all_articles[idx]["wp_upload_date"] = datetime.now().isoformat()
all_articles[idx]["wp_post_id"] = wp_post_id
break
save_articles(all_articles)
show_notification("✅ Erfolgreich zu WordPress hochgeladen!")
else:
show_notification(f"❌ WordPress-Upload fehlgeschlagen: {message}", "error")
time.sleep(1)
st.rerun()
with col5:
# Details anzeigen
if st.button("📖 Details", key=f"details_{article['id']}"):
st.session_state[f"show_details_{article['id']}"] = not st.session_state.get(f"show_details_{article['id']}", False)
# Details Section (wenn erweitert)
if st.session_state.get(f"show_details_{article['id']}", False):
st.markdown("---")
# Artikel Text
with st.expander("📝 Volltext", expanded=False):
st.code(article.get("text", ""), language="markdown")
# Tags bearbeiten
with st.expander("🏷️ Tags bearbeiten", expanded=False):
current_tags = ", ".join(article.get("tags", []))
new_tags = st.text_area("Tags (getrennt durch Komma)", value=current_tags, key=f"tags_{article['id']}")
if st.button("Tags speichern", key=f"save_tags_{article['id']}"):
tag_list = [tag.strip() for tag in new_tags.split(",") if tag.strip()]
for idx, art in enumerate(all_articles):
if art["id"] == article["id"]:
all_articles[idx]["tags"] = tag_list
break
save_articles(all_articles)
show_notification("Tags gespeichert!")
st.rerun()
# Bilder
if article.get("images"):
with st.expander("🖼️ Bilder verwalten", expanded=False):
for i, img in enumerate(article.get("images", [])):
col1, col2 = st.columns([1, 2])
with col1:
st.image(img["url"], width=200)
with col2:
caption = st.text_input("Bildtitel", value=img.get("caption", ""), key=f"caption_{article['id']}_{i}")
copyright_text = st.text_input("Copyright", value=img.get("copyright", ""), key=f"copyright_{article['id']}_{i}")
copyright_url = st.text_input("Quelle URL", value=img.get("copyright_url", ""), key=f"copyright_url_{article['id']}_{i}")
if st.button("Bilddaten speichern", key=f"save_img_{article['id']}_{i}"):
img["caption"] = caption or "Kein Bildtitel vorhanden"
img["copyright"] = copyright_text or "Unbekannt"
img["copyright_url"] = copyright_url or "#"
for idx, art in enumerate(all_articles):
if art["id"] == article["id"]:
all_articles[idx] = article
break
save_articles(all_articles)
show_notification("Bilddaten gespeichert!")
st.rerun()
# DALL-E Bildgenerierung
if st.button("🪄 KI-Bild generieren", key=f"dalle_{article['id']}"):
if not any(img.get("copyright") == "OpenAI DALL·E" for img in article.get("images", [])):
with st.spinner("Bild wird generiert..."):
prompt = article["title"]
image_url = generate_dalle_image(prompt)
if image_url:
article.setdefault("images", []).append({
"url": image_url,
"alt": f"KI-generiertes Titelbild zu: {prompt}",
"caption": f"KI-generiertes Titelbild zu: {prompt}",
"copyright": "OpenAI DALL·E",
"copyright_url": "https://openai.com/dall-e"
})
for idx, art in enumerate(all_articles):
if art["id"] == article["id"]:
all_articles[idx] = article
break
save_articles(all_articles)
show_notification("DALL·E-Bild erfolgreich hinzugefügt!")
st.rerun()
else:
show_notification("Fehler beim Erzeugen des Bildes.", "error")
else:
show_notification("Ein KI-generiertes Bild ist bereits vorhanden.", "info")
st.markdown('</div>', unsafe_allow_html=True)
# === Feeds Tab ===
with tab3:
st.header("📡 RSS Feeds verwalten")
# Feed hinzufügen
with st.expander(" Neuen Feed hinzufügen", expanded=False):
col1, col2 = st.columns(2)
with col1:
new_url = st.text_input("Feed URL")
with col2:
new_name = st.text_input("Feed Name")
if st.button("Feed hinzufügen", use_container_width=True):
if new_url and new_name:
if not any(f.get("url") == new_url for f in feeds):
feeds.append({"url": new_url, "name": new_name})
save_feeds(feeds)
show_notification(f"Feed '{new_name}' hinzugefügt!")
st.rerun()
else:
show_notification("Dieser Feed existiert bereits.", "warning")
else:
show_notification("Bitte URL und Name eingeben.", "error")
# Feeds anzeigen
for idx, feed in enumerate(feeds):
feed_url = feed.get("url", "")
feed_name = feed.get("name", "Unbekannt")
article_count = sum(1 for a in all_articles if a.get("source") == feed_url)
st.markdown(f"""
<div class="article-card">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<strong>{feed_name}</strong>
<br>
<small>{feed_url}</small>
<br>
<span style="color: #667eea;">📰 {article_count} Artikel</span>
</div>
<div>
<span class="status-badge status-online">{article_count} Artikel</span>
</div>
</div>
</div>
""", unsafe_allow_html=True)
# Feed Actions
col1, col2, col3 = st.columns(3)
with col1:
if st.button("✏️ Bearbeiten", key=f"edit_feed_{idx}"):
st.session_state[f"edit_feed_{idx}"] = not st.session_state.get(f"edit_feed_{idx}", False)
with col2:
if st.button("🔄 Aktualisieren", key=f"refresh_feed_{idx}"):
with st.spinner("Feed wird aktualisiert..."):
existing_ids = [a["id"] for a in all_articles]
# Hier könntest du eine einzelne Feed-Update-Funktion implementieren
process_articles(existing_ids)
show_notification(f"Feed '{feed_name}' aktualisiert!")
st.rerun()
with col3:
if st.button("🗑️ Löschen", key=f"delete_feed_{idx}"):
feeds.pop(idx)
save_feeds(feeds)
show_notification(f"Feed '{feed_name}' gelöscht!", "warning")
st.rerun()
# Edit Form
if st.session_state.get(f"edit_feed_{idx}", False):
with st.form(f"edit_form_{idx}"):
new_feed_url = st.text_input("URL", value=feed_url)
new_feed_name = st.text_input("Name", value=feed_name)
if st.form_submit_button("Änderungen speichern"):
feeds[idx]["url"] = new_feed_url
feeds[idx]["name"] = new_feed_name
save_feeds(feeds)
show_notification("Feed aktualisiert!")
st.session_state[f"edit_feed_{idx}"] = False
st.rerun()
# === Bilder Tab ===
with tab4:
st.header("🖼️ Bilderverwaltung")
# Alle Bilder sammeln
all_images = []
for article in all_articles:
for img in article.get("images", []):
img_data = img.copy()
img_data["article_title"] = article.get("title", "Unbekannt")
img_data["article_id"] = article.get("id")
all_images.append(img_data)
if all_images:
st.write(f"**{len(all_images)} Bilder gefunden**")
# Bilder in Spalten anzeigen
cols = st.columns(3)
for idx, img in enumerate(all_images):
with cols[idx % 3]:
st.image(img["url"], use_column_width=True)
st.markdown(f"**{img.get('caption', 'Kein Titel')}**")
st.markdown(f"📰 {img['article_title']}")
st.markdown(f"©️ {img.get('copyright', 'Unbekannt')}")
if img.get("copyright_url") and img["copyright_url"] != "#":
st.markdown(f"[🔗 Quelle]({img['copyright_url']})")
else:
st.info("Keine Bilder gefunden.")
# === Statistiken Tab ===
with tab5:
st.header("📊 Detaillierte Statistiken")
# Status Verteilung
status_counts = Counter([a.get("status", "New") for a in all_articles])
col1, col2 = st.columns(2)
with col1:
st.subheader("📈 Status Verteilung")
for status, count in status_counts.items():
percentage = (count / len(all_articles) * 100) if all_articles else 0
st.markdown(f"{get_status_badge(status)} {count} ({percentage:.1f}%)", unsafe_allow_html=True)
with col2:
st.subheader("📡 Artikel pro Feed")
feed_counts = Counter([source_to_name.get(a.get("source", ""), "Unbekannt") for a in all_articles])
for feed_name, count in feed_counts.most_common():
st.markdown(f"**{feed_name}:** {count} Artikel")
# WordPress-Statistiken
st.subheader("🔗 WordPress-Statistiken")
wp_articles = [a for a in all_articles if a.get("wp_post_id")]
if wp_articles:
col1, col2, col3 = st.columns(3)
with col1:
st.metric("WordPress Artikel", len(wp_articles))
with col2:
pending_count = len([a for a in wp_articles if a.get("status") == "WordPress Pending"])
st.metric("Ausstehend", pending_count)
with col3:
online_wp_count = len([a for a in wp_articles if a.get("status") == "Online"])
st.metric("Online", online_wp_count)
# Neueste WordPress-Uploads
recent_wp = sorted([a for a in wp_articles if a.get("wp_upload_date")],
key=lambda x: x.get("wp_upload_date", ""), reverse=True)[:5]
if recent_wp:
st.subheader("🕒 Neueste WordPress-Uploads")
for article in recent_wp:
st.markdown(f"""
**{article.get('title', 'Kein Titel')}** {get_status_badge(article.get('status', 'Unknown'))}
WP ID: {article.get('wp_post_id')} | Upload: {format_date(article.get('wp_upload_date', ''))}
""", unsafe_allow_html=True)
else:
st.info("Noch keine Artikel zu WordPress hochgeladen.")
# Weitere Statistiken
st.subheader("📝 Textstatistiken")
word_counts = [get_word_count(a.get("text", "")) for a in all_articles]
if word_counts:
col1, col2, col3 = st.columns(3)
with col1:
st.metric("Durchschnittliche Wortanzahl", f"{sum(word_counts) // len(word_counts)}")
with col2:
st.metric("Längster Artikel", f"{max(word_counts)} Wörter")
with col3:
st.metric("Kürzester Artikel", f"{min(word_counts)} Wörter")
# Tag Cloud Simulation
st.subheader("🏷️ Häufigste Tags")
all_tags = []
for article in all_articles:
all_tags.extend(article.get("tags", []))
if all_tags:
tag_counts = Counter(all_tags)
for tag, count in tag_counts.most_common(10):
st.markdown(f"**{tag}:** {count}x verwendet")
else:
st.info("Keine Tags gefunden.")
# === WordPress Tab ===
with tab6:
st.header("🔧 WordPress-Integration")
# Verbindungstest
st.subheader("🔗 Verbindungstest")
col1, col2 = st.columns(2)
with col1:
if st.button("🧪 WordPress-Verbindung testen", use_container_width=True):
with st.spinner("Teste Verbindung..."):
success, message = test_wordpress_connection()
if success:
show_notification(f"{message}")
else:
show_notification(f"{message}", "error")
with col2:
# WordPress-Konfiguration anzeigen
wp_url = os.getenv("WP_BASE_URL", "Nicht konfiguriert")
wp_user = os.getenv("WP_USERNAME", "Nicht konfiguriert")
wp_base64 = os.getenv("WP_AUTH_BASE64", "")
st.info(f"""
**WordPress-Konfiguration:**
- URL: {wp_url}
- Benutzer: {wp_user}
- Passwort: {'✅ Konfiguriert' if os.getenv("WP_PASSWORD") else '❌ Nicht konfiguriert'}
- Base64 Auth: {'✅ Konfiguriert' if wp_base64 else '❌ Nicht konfiguriert'}
""")
# WordPress Auth Debug (nur für Entwicklung)
if st.checkbox("🔧 Debug-Modus (Auth-Details anzeigen)", value=False):
st.warning("⚠️ Nur für Entwicklung - zeigt Auth-Details!")
wp_base64 = os.getenv("WP_AUTH_BASE64", "")
if wp_base64:
try:
import base64
decoded = base64.b64decode(wp_base64).decode('utf-8')
st.code(f"Base64: {wp_base64}\nDecoded: {decoded}")
except Exception as e:
st.error(f"Fehler beim Dekodieren: {e}")
else:
st.info("Kein Base64-String konfiguriert")
# Bulk Upload
st.subheader("📦 Massenupload")
process_articles_list = [a for a in all_articles if a.get("status") == "Process"]
if process_articles_list:
st.write(f"**{len(process_articles_list)} Artikel bereit für WordPress-Upload:**")
# Artikel-Vorschau
for article in process_articles_list[:5]: # Nur die ersten 5 anzeigen
st.markdown(f"• **{article.get('title', 'Kein Titel')}** ({get_word_count(article.get('text', ''))} Wörter)")
if len(process_articles_list) > 5:
st.markdown(f"... und {len(process_articles_list) - 5} weitere")
col1, col2 = st.columns(2)
with col1:
if st.button("📤 Alle zu WordPress hochladen", use_container_width=True):
with st.spinner(f"Lade {len(process_articles_list)} Artikel zu WordPress hoch..."):
upload_results = upload_articles_to_wp()
# Detaillierte Ergebnisse anzeigen
st.subheader("📊 Upload-Ergebnisse")
if upload_results.get('error'):
show_notification(f"❌ Fehler: {upload_results['error']}", "error")
else:
col1, col2, col3 = st.columns(3)
with col1:
st.metric("Erfolgreich", upload_results.get('successful', 0))
with col2:
st.metric("Fehlgeschlagen", upload_results.get('failed', 0))
with col3:
st.metric("Duplikate", upload_results.get('duplicates', 0))
# Details anzeigen
if upload_results.get('details'):
st.subheader("📋 Upload-Details")
for detail in upload_results['details']:
status_icon = "" if detail['success'] else ""
st.markdown(f"{status_icon} **{detail['title']}**: {detail['message']}")
time.sleep(2)
st.rerun()
with col2:
st.info("💡 Artikel erhalten den Status 'WordPress Pending' nach erfolgreichem Upload.")
else:
st.info("Keine Artikel mit Status 'Process' gefunden. Artikel müssen zuerst umgeschrieben werden.")
# WordPress-Artikel-Übersicht
st.subheader("📋 WordPress-Artikel-Übersicht")
wp_articles = [a for a in all_articles if a.get("wp_post_id")]
if wp_articles:
# Filter für WordPress-Artikel
wp_status_filter = st.selectbox(
"WordPress-Status filtern",
["Alle", "WordPress Pending", "Online"],
key="wp_status_filter"
)
filtered_wp_articles = wp_articles
if wp_status_filter != "Alle":
filtered_wp_articles = [a for a in wp_articles if a.get("status") == wp_status_filter]
st.write(f"**{len(filtered_wp_articles)} WordPress-Artikel gefunden**")
# WordPress-Artikel anzeigen
for article in filtered_wp_articles:
st.markdown(f"""
<div class="wp-status">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<strong>{article.get('title', 'Kein Titel')}</strong>
<br>
<small>WP ID: {article.get('wp_post_id')} | Upload: {format_date(article.get('wp_upload_date', ''))}</small>
</div>
<div>
{get_status_badge(article.get('status', 'Unknown'))}
</div>
</div>
</div>
""", unsafe_allow_html=True)
else:
st.info("Noch keine Artikel zu WordPress hochgeladen.")
# Konfigurationshilfe
st.subheader("⚙️ Konfiguration")
with st.expander("📋 .env-Datei Vorlage", expanded=False):
st.code("""
# WordPress-Konfiguration
WP_BASE_URL=https://vanityontour.de
WP_USERNAME=ogiertz
WP_PASSWORD=whNEx9aZCIUXViV89Z3e7Z03
# WordPress Base64-Authentifizierung (bevorzugte Methode)
WP_AUTH_BASE64=b2dpZXJ0ejp3aE5FeDlhWkNJVVhWaVY4OVozZTdaMDM=
# OpenAI-Konfiguration (für Artikel-Umschreibung)
OPENAI_API_KEY=sk-...
""", language="bash")
with st.expander("🔑 Base64-Authentifizierung verstehen", expanded=False):
st.markdown("""
**WordPress REST API Authentifizierung:**
Die WordPress REST API erfordert eine Base64-kodierte Authentifizierung im Format:
```
Authorization: Basic <base64_encoded_credentials>
```
**Ihr bereitgestellter Base64-String:**
- `b2dpZXJ0ejp3aE5FeDlhWkNJVVhWaVY4OVozZTdaMDM=`
- Dekodiert: `ogiertz:whNEx9aZCIUXViV89Z3e7Z03`
**So funktioniert es:**
1. Benutzername und Anwendungspasswort werden kombiniert: `username:password`
2. Dieser String wird Base64-kodiert
3. Im Authorization-Header verwendet: `Basic <base64_string>`
**Fallback-Verhalten:**
- Wenn `WP_AUTH_BASE64` gesetzt ist → Direkter Base64-String verwendet
- Wenn nicht gesetzt → Base64 wird aus `WP_USERNAME:WP_PASSWORD` generiert
""")
with st.expander("📖 WordPress-API Berechtigungen", expanded=False):
st.markdown("""
**Erforderliche Berechtigungen für den WordPress-Benutzer:**
- `edit_posts` - Beiträge erstellen und bearbeiten
- `publish_posts` - Beiträge veröffentlichen (für Status-Änderungen)
- `upload_files` - Dateien hochladen (für spätere Bild-Uploads)
- `edit_categories` - Kategorien verwalten
- `edit_tags` - Tags verwalten
**Anwendungspasswort erstellen:**
1. WordPress Admin → Benutzer → Profil
2. Unter "Anwendungspasswörter" neues Passwort erstellen
3. Name: "RSS Feed Manager"
4. Generiertes Passwort in .env-Datei eintragen
""")