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:
61
src/feeds/transcript_extractors/website_dlf.py
Normale Datei
61
src/feeds/transcript_extractors/website_dlf.py
Normale Datei
@@ -0,0 +1,61 @@
|
||||
"""Deutschlandfunk: Manuskripte auf den Sender-Websites.
|
||||
|
||||
Domains:
|
||||
- deutschlandfunk.de
|
||||
- deutschlandfunkkultur.de
|
||||
- deutschlandfunknova.de
|
||||
|
||||
Dlf-Artikel-HTML enthaelt den Manuskript-Text typischerweise in
|
||||
<article class="b-article">...</article> mit vielen <p>-Absaetzen
|
||||
oder als <div class="b-text">. Als Fallback greift der generische
|
||||
Longest-Article-Block-Extraktor.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from . import TranscriptResult
|
||||
from ._common import (
|
||||
episode_url,
|
||||
extract_longest_article_block,
|
||||
extract_text_by_container,
|
||||
fetch_html,
|
||||
matches_domain,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("osint.podcast.extractors.dlf")
|
||||
|
||||
_DOMAINS = (
|
||||
"deutschlandfunk.de",
|
||||
"deutschlandfunkkultur.de",
|
||||
"deutschlandfunknova.de",
|
||||
)
|
||||
|
||||
_CONTAINER_PATTERNS = [
|
||||
r'<article[^>]*class="[^"]*b-article[^"]*"[^>]*>',
|
||||
r'<div[^>]*class="[^"]*b-text[^"]*"[^>]*>',
|
||||
r'<article\b[^>]*>',
|
||||
r'<main\b[^>]*>',
|
||||
]
|
||||
|
||||
|
||||
def can_handle(feed_entry: dict, feed_url: str) -> bool:
|
||||
url = episode_url(feed_entry) or feed_url
|
||||
return matches_domain(url, _DOMAINS) or matches_domain(feed_url, _DOMAINS)
|
||||
|
||||
|
||||
async def fetch(feed_entry: dict, feed_url: str) -> Optional[TranscriptResult]:
|
||||
url = episode_url(feed_entry)
|
||||
if not url:
|
||||
return None
|
||||
html = await fetch_html(url)
|
||||
if not html:
|
||||
return None
|
||||
|
||||
text = extract_text_by_container(html, _CONTAINER_PATTERNS)
|
||||
if not text:
|
||||
text = extract_longest_article_block(html)
|
||||
if not text:
|
||||
return None
|
||||
return TranscriptResult(text=text, source="website_scrape")
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren