diff --git a/src/fabledassistant/services/rss.py b/src/fabledassistant/services/rss.py index 1c02f16..39457a4 100644 --- a/src/fabledassistant/services/rss.py +++ b/src/fabledassistant/services/rss.py @@ -66,6 +66,8 @@ async def fetch_and_cache_feed(feed_id: int, url: str) -> int: return 0 new_count = 0 + feed_user_id: int | None = None + async with async_session() as session: for entry in parsed.entries: item_data = extract_item(entry) @@ -91,14 +93,38 @@ async def fetch_and_cache_feed(feed_id: int, url: str) -> int: feed_row = await session.get(RssFeed, feed_id) if feed_row: feed_row.last_fetched_at = datetime.now(timezone.utc) + feed_user_id = feed_row.user_id # Auto-populate title from feed metadata if blank if not feed_row.title and parsed.feed.get("title"): feed_row.title = parsed.feed.title[:200] await session.commit() + # Collect IDs of unclassified items after commit. + # We query classified_at IS NULL (not just the items inserted above) because + # classification is best-effort and may have failed on previous fetches. + # Re-queuing all unclassified items for this feed on each fetch is intentional: + # it provides automatic retry without a separate retry loop. The classifier + # only writes to items it successfully classifies, so already-classified items + # are not re-processed (they have classified_at set). + unclassified_ids: list[int] = [] + if new_count > 0: + result = await session.execute( + select(RssItem.id).where( + RssItem.feed_id == feed_id, + RssItem.classified_at.is_(None), + ) + ) + unclassified_ids = list(result.scalars().all()) + # Prune old items to keep DB tidy await _prune_old_items(feed_id) + + # Fire-and-forget classification for unclassified items + if unclassified_ids and feed_user_id is not None: + from fabledassistant.services.rss_classifier import classify_and_store + asyncio.create_task(classify_and_store(unclassified_ids, feed_user_id)) + return new_count