54 lines
1.9 KiB
Rust
54 lines
1.9 KiB
Rust
|
|
use anyhow::Result;
|
||
|
|
use chrono::{DateTime, Utc};
|
||
|
|
use feedsignal_core::Article;
|
||
|
|
use uuid::Uuid;
|
||
|
|
|
||
|
|
/// Fetches and parses one RSS/Atom feed. `feed-rs` normalizes both formats
|
||
|
|
/// into a common model so callers don't need to branch on feed type.
|
||
|
|
pub async fn fetch_feed(feed_url: &str, feed_id: Uuid) -> Result<Vec<Article>> {
|
||
|
|
let bytes = reqwest::get(feed_url).await?.bytes().await?;
|
||
|
|
let parsed = feed_rs::parser::parse(&bytes[..])?;
|
||
|
|
|
||
|
|
let articles = parsed
|
||
|
|
.entries
|
||
|
|
.into_iter()
|
||
|
|
.filter_map(|entry| {
|
||
|
|
let url = entry.links.first()?.href.clone();
|
||
|
|
let title = entry.title.map(|t| t.content).unwrap_or_default();
|
||
|
|
let summary = entry
|
||
|
|
.summary
|
||
|
|
.map(|s| s.content)
|
||
|
|
.or_else(|| entry.content.and_then(|c| c.body))
|
||
|
|
.unwrap_or_default();
|
||
|
|
let published_at: Option<DateTime<Utc>> = entry.published.or(entry.updated);
|
||
|
|
let estimated_read_seconds = Some(estimate_read_seconds(&summary));
|
||
|
|
|
||
|
|
Some(Article {
|
||
|
|
id: Uuid::new_v4(),
|
||
|
|
feed_id,
|
||
|
|
url,
|
||
|
|
title,
|
||
|
|
summary,
|
||
|
|
published_at,
|
||
|
|
topics: Vec::new(),
|
||
|
|
embedding_score: None,
|
||
|
|
llm_score: None,
|
||
|
|
final_score: None,
|
||
|
|
estimated_read_seconds,
|
||
|
|
})
|
||
|
|
})
|
||
|
|
.collect();
|
||
|
|
|
||
|
|
Ok(articles)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Rough estimate from word count at ~200 wpm, used as the denominator for
|
||
|
|
/// dwell-time-based engagement scoring (see `feedsignal_core::affinity`).
|
||
|
|
/// This is a placeholder — the full article body isn't fetched here, so
|
||
|
|
/// it's a floor rather than an accurate estimate until content extraction
|
||
|
|
/// is added.
|
||
|
|
fn estimate_read_seconds(text: &str) -> u32 {
|
||
|
|
let words = text.split_whitespace().count() as u32;
|
||
|
|
((words.max(1) as f32 / 200.0) * 60.0) as u32
|
||
|
|
}
|