Podcast-Integration Phase 1: Feed-Tag + Senderseiten
Podcasts werden wie normale RSS-Quellen behandelt (source_type=podcast_feed).
Kein externer bezahlter Dienst, keine lokale Transkription — Monitor nutzt
ausschliesslich vorhandene Transkripte.
Kaskade fuer Transkript-Bezug:
1. Podcasting-2.0-Tag <podcast:transcript> im Feed (SRT/VTT/HTML/JSON)
2. Redaktionelles Manuskript auf der Episodenseite
(Adapter: Dlf, SZ, Spiegel, NDR)
3. YouTube-Captions — Phase 2, optional per yt-dlp
Kein Stufen-Treffer -> Episode verworfen (graceful, kein Error).
Neu:
- src/feeds/podcast_parser.py (eigener Parser, RSS-Heisspfad unveraendert)
- src/feeds/transcript_extractors/ (Plugin-Muster):
__init__.py Dispatcher, Cache-Lookup gegen podcast_transcripts
_common.py HTML-Extraktion, Domain-Matching, httpx-Helper
rss_native.py Stufe 1: Feed-Tag-Parser (SRT/VTT/JSON/HTML)
website_dlf.py Stufe 2: deutschlandfunk.de + Schwester-Domains
website_sz.py Stufe 2: sz.de / sueddeutsche.de
website_spiegel.py Stufe 2: spiegel.de / manager-magazin.de
website_ndr.py Stufe 2: ndr.de
Geaendert:
- src/database.py: idempotente Migration, Tabelle podcast_transcripts als
URL-Cache gegen Mehrfach-Scrape zwischen Lagen
- src/models.py: Pydantic-Pattern von source_type um podcast_feed erweitert
- src/source_rules.py: get_feeds_with_metadata() nimmt source_type-Parameter,
Default rss_feed (RSS-Pfad unveraendert)
- src/agents/orchestrator.py: neue _podcast_pipeline() parallel zu RSS,
WebSearch und Telegram; nur fuer adhoc-Lagen; ohne Podcast-Quellen dormant
Verifikation:
- Migration auf Live-DB erfolgreich (Log: Tabelle podcast_transcripts angelegt)
- Import-/Instanziierungs-Test aller Module bestanden
- can_handle-Tests pro Sender-Adapter positiv + negativ OK
- Live-Scrape gegen Dlf: 22710 Zeichen, gegen SZ: 24918 Zeichen
- Dormant-Test: 0 Podcast-Quellen -> keine neue Codezeile im Refresh
Verwerfbarkeit: rein additiv, RSS-Pfad unberuehrt, Rollback in drei
Schritten (Quellen disablen, git revert, DROP TABLE podcast_transcripts).
Dieser Commit ist enthalten in:
@@ -637,8 +637,12 @@ def _fallback_all_feeds(domain: str, feeds: list[dict]) -> list[dict]:
|
||||
]
|
||||
|
||||
|
||||
async def get_feeds_with_metadata(tenant_id: int = None) -> list[dict]:
|
||||
"""Alle aktiven RSS-Feeds mit Metadaten fuer Claude-Selektion (global + org-spezifisch)."""
|
||||
async def get_feeds_with_metadata(tenant_id: int = None, source_type: str = "rss_feed") -> list[dict]:
|
||||
"""Aktive Feeds eines bestimmten Typs mit Metadaten fuer Claude-Selektion (global + org-spezifisch).
|
||||
|
||||
source_type: "rss_feed" (Default) oder "podcast_feed" — trennt RSS- und Podcast-Quellen
|
||||
in getrennten Pipelines, damit der RSS-Heisspfad unveraendert bleibt.
|
||||
"""
|
||||
from database import get_db
|
||||
|
||||
db = await get_db()
|
||||
@@ -646,18 +650,19 @@ async def get_feeds_with_metadata(tenant_id: int = None) -> list[dict]:
|
||||
if tenant_id:
|
||||
cursor = await db.execute(
|
||||
"SELECT name, url, domain, category, COALESCE(article_count, 0) AS article_count FROM sources "
|
||||
"WHERE source_type = 'rss_feed' AND status = 'active' "
|
||||
"WHERE source_type = ? AND status = 'active' "
|
||||
"AND (tenant_id IS NULL OR tenant_id = ?)",
|
||||
(tenant_id,),
|
||||
(source_type, tenant_id),
|
||||
)
|
||||
else:
|
||||
cursor = await db.execute(
|
||||
"SELECT name, url, domain, category, COALESCE(article_count, 0) AS article_count FROM sources "
|
||||
"WHERE source_type = 'rss_feed' AND status = 'active'"
|
||||
"WHERE source_type = ? AND status = 'active'",
|
||||
(source_type,),
|
||||
)
|
||||
return [dict(row) for row in await cursor.fetchall()]
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden der Feed-Metadaten: {e}")
|
||||
logger.error(f"Fehler beim Laden der Feed-Metadaten ({source_type}): {e}")
|
||||
return []
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren