import asyncio
import logging
import re
from html import unescape
from typing import List, Optional
import feedparser
from src.domain.news.entities import NewsItem
from src.domain.news.ports import NewsFetcher
from src.domain.subscriptions.entities import Subscription
logger = logging.getLogger(__name__)
def _strip_html(text: str) -> str:
"""Удаляет HTML-теги и декодирует HTML-сущности."""
text = re.sub(r'<[^>]+>', '', text)
return unescape(text).strip()
def _extract_image_url(entry) -> Optional[str]:
"""Возвращает URL первого enclosure с type image/*."""
for enc in entry.get("enclosures", []):
if enc.get("type", "").startswith("image/"):
return enc.get("href") or enc.get("url")
return None
class RssFetcher(NewsFetcher):
async def fetch(self, subscription: Subscription) -> List[NewsItem]:
"""
Опрашивает RSS-ленту. feedparser.parse() — блокирующий вызов,
поэтому запускается в executor чтобы не блокировать event loop.
"""
loop = asyncio.get_event_loop()
try:
feed = await loop.run_in_executor(None, feedparser.parse, subscription.source)
except Exception:
logger.exception("Ошибка при чтении RSS %s", subscription.source)
return []
items = []
for entry in feed.entries:
news_id = entry.get("id") or entry.get("link") or ""
title = entry.get("title", "(без заголовка)")
link = entry.get("link", "")
raw_summary = entry.get("summary")
summary = _strip_html(raw_summary) if raw_summary else None
image_url = _extract_image_url(entry)
if news_id:
items.append(NewsItem(
id=news_id,
title=title,
link=link,
summary=summary,
image_url=image_url,
))
return items