310 lines
9.8 KiB
Rust
310 lines
9.8 KiB
Rust
use crate::models::{ClientEvent, Sermon, Bulletin, BibleVerse};
|
|
use chrono::{DateTime, Utc, NaiveDateTime};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct FeedItem {
|
|
pub id: String,
|
|
pub feed_type: FeedItemType,
|
|
pub timestamp: String, // ISO8601 format
|
|
pub priority: i32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "type")]
|
|
pub enum FeedItemType {
|
|
#[serde(rename = "event")]
|
|
Event {
|
|
event: ClientEvent,
|
|
},
|
|
#[serde(rename = "sermon")]
|
|
Sermon {
|
|
sermon: Sermon,
|
|
},
|
|
#[serde(rename = "bulletin")]
|
|
Bulletin {
|
|
bulletin: Bulletin,
|
|
},
|
|
#[serde(rename = "verse")]
|
|
Verse {
|
|
verse: BibleVerse,
|
|
},
|
|
}
|
|
|
|
/// Parse date string to DateTime<Utc>, with fallback to current time
|
|
fn parse_date_with_fallback(date_str: &str) -> DateTime<Utc> {
|
|
// Try ISO8601 format first
|
|
if let Ok(dt) = DateTime::parse_from_rfc3339(date_str) {
|
|
return dt.with_timezone(&Utc);
|
|
}
|
|
|
|
// Try naive datetime parsing
|
|
if let Ok(naive) = NaiveDateTime::parse_from_str(date_str, "%Y-%m-%d %H:%M:%S") {
|
|
return DateTime::from_naive_utc_and_offset(naive, Utc);
|
|
}
|
|
|
|
// Fallback to current time
|
|
Utc::now()
|
|
}
|
|
|
|
/// Calculate priority for feed items based on type and recency
|
|
fn calculate_priority(feed_type: &FeedItemType, timestamp: &DateTime<Utc>) -> i32 {
|
|
let now = Utc::now();
|
|
let age_days = (now - *timestamp).num_days().max(0);
|
|
|
|
match feed_type {
|
|
FeedItemType::Event { .. } => {
|
|
// Events get highest priority, especially upcoming ones
|
|
if *timestamp > now {
|
|
1000 // Future events (upcoming)
|
|
} else {
|
|
800 - (age_days as i32) // Recent past events
|
|
}
|
|
},
|
|
FeedItemType::Sermon { .. } => {
|
|
// Sermons get high priority when recent
|
|
600 - (age_days as i32)
|
|
},
|
|
FeedItemType::Bulletin { .. } => {
|
|
// Bulletins get medium priority
|
|
400 - (age_days as i32)
|
|
},
|
|
FeedItemType::Verse { .. } => {
|
|
// Daily verse always gets consistent priority
|
|
300
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Aggregate and sort home feed items
|
|
pub fn aggregate_home_feed(
|
|
events: &[ClientEvent],
|
|
sermons: &[Sermon],
|
|
bulletins: &[Bulletin],
|
|
daily_verse: Option<&BibleVerse>
|
|
) -> Vec<FeedItem> {
|
|
let mut feed_items = Vec::new();
|
|
|
|
// Add recent sermons (limit to 3)
|
|
for sermon in sermons.iter().take(3) {
|
|
let timestamp = sermon.date; // Already a DateTime<Utc>
|
|
let feed_type = FeedItemType::Sermon { sermon: sermon.clone() };
|
|
let priority = calculate_priority(&feed_type, ×tamp);
|
|
|
|
feed_items.push(FeedItem {
|
|
id: format!("sermon_{}", sermon.id),
|
|
feed_type,
|
|
timestamp: timestamp.to_rfc3339(),
|
|
priority,
|
|
});
|
|
}
|
|
|
|
// Add upcoming events (limit to 2)
|
|
for event in events.iter().take(2) {
|
|
let timestamp = parse_date_with_fallback(&event.created_at);
|
|
let feed_type = FeedItemType::Event { event: event.clone() };
|
|
let priority = calculate_priority(&feed_type, ×tamp);
|
|
|
|
feed_items.push(FeedItem {
|
|
id: format!("event_{}", event.id),
|
|
feed_type,
|
|
timestamp: timestamp.to_rfc3339(),
|
|
priority,
|
|
});
|
|
}
|
|
|
|
// Add most recent bulletin
|
|
if let Some(bulletin) = bulletins.first() {
|
|
let timestamp = parse_date_with_fallback(&bulletin.date.to_string());
|
|
let feed_type = FeedItemType::Bulletin { bulletin: bulletin.clone() };
|
|
let priority = calculate_priority(&feed_type, ×tamp);
|
|
|
|
feed_items.push(FeedItem {
|
|
id: format!("bulletin_{}", bulletin.id),
|
|
feed_type,
|
|
timestamp: timestamp.to_rfc3339(),
|
|
priority,
|
|
});
|
|
}
|
|
|
|
// Add daily verse
|
|
if let Some(verse) = daily_verse {
|
|
let timestamp = Utc::now();
|
|
let feed_type = FeedItemType::Verse { verse: verse.clone() };
|
|
let priority = calculate_priority(&feed_type, ×tamp);
|
|
|
|
feed_items.push(FeedItem {
|
|
id: format!("verse_{}", verse.reference),
|
|
feed_type,
|
|
timestamp: timestamp.to_rfc3339(),
|
|
priority,
|
|
});
|
|
}
|
|
|
|
// Sort by priority (highest first), then by timestamp (newest first)
|
|
feed_items.sort_by(|a, b| {
|
|
b.priority.cmp(&a.priority)
|
|
.then_with(|| b.timestamp.cmp(&a.timestamp))
|
|
});
|
|
|
|
feed_items
|
|
}
|
|
|
|
/// Media type enumeration for content categorization
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum MediaType {
|
|
Sermons,
|
|
LiveStreams,
|
|
}
|
|
|
|
impl MediaType {
|
|
pub fn display_name(&self) -> &'static str {
|
|
match self {
|
|
MediaType::Sermons => "Sermons",
|
|
MediaType::LiveStreams => "Live Archives",
|
|
}
|
|
}
|
|
|
|
pub fn icon_name(&self) -> &'static str {
|
|
match self {
|
|
MediaType::Sermons => "play.rectangle.fill",
|
|
MediaType::LiveStreams => "dot.radiowaves.left.and.right",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Get sermons or livestreams based on media type
|
|
pub fn get_media_content(sermons: &[Sermon], media_type: &MediaType) -> Vec<Sermon> {
|
|
match media_type {
|
|
MediaType::Sermons => {
|
|
// Filter for regular sermons (non-livestream)
|
|
sermons.iter()
|
|
.filter(|sermon| !sermon.title.to_lowercase().contains("livestream"))
|
|
.cloned()
|
|
.collect()
|
|
},
|
|
MediaType::LiveStreams => {
|
|
// Filter for livestream archives
|
|
sermons.iter()
|
|
.filter(|sermon| sermon.title.to_lowercase().contains("livestream"))
|
|
.cloned()
|
|
.collect()
|
|
},
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::models::{ClientEvent, Sermon, Bulletin, BibleVerse};
|
|
|
|
fn create_sample_event(id: &str, title: &str) -> ClientEvent {
|
|
ClientEvent {
|
|
id: id.to_string(),
|
|
title: title.to_string(),
|
|
description: "Sample description".to_string(),
|
|
date: "2025-01-15".to_string(),
|
|
start_time: "6:00 PM".to_string(),
|
|
end_time: "8:00 PM".to_string(),
|
|
location: "Sample Location".to_string(),
|
|
location_url: None,
|
|
image: None,
|
|
thumbnail: None,
|
|
category: "Social".to_string(),
|
|
is_featured: false,
|
|
recurring_type: None,
|
|
tags: None,
|
|
contact_email: None,
|
|
contact_phone: None,
|
|
registration_url: None,
|
|
max_attendees: None,
|
|
current_attendees: None,
|
|
created_at: "2025-01-10T10:00:00Z".to_string(),
|
|
updated_at: "2025-01-10T10:00:00Z".to_string(),
|
|
duration_minutes: 120,
|
|
has_registration: false,
|
|
is_full: false,
|
|
spots_remaining: None,
|
|
}
|
|
}
|
|
|
|
fn create_sample_sermon(id: &str, title: &str) -> Sermon {
|
|
Sermon {
|
|
id: id.to_string(),
|
|
title: title.to_string(),
|
|
description: Some("Sample sermon".to_string()),
|
|
date: Some("2025-01-10T10:00:00Z".to_string()),
|
|
video_url: Some("https://example.com/video".to_string()),
|
|
audio_url: None,
|
|
thumbnail_url: None,
|
|
duration: None,
|
|
speaker: Some("Pastor Smith".to_string()),
|
|
series: None,
|
|
scripture_references: None,
|
|
tags: None,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_aggregate_home_feed() {
|
|
let events = vec![
|
|
create_sample_event("1", "Event 1"),
|
|
create_sample_event("2", "Event 2"),
|
|
];
|
|
|
|
let sermons = vec![
|
|
create_sample_sermon("1", "Sermon 1"),
|
|
create_sample_sermon("2", "Sermon 2"),
|
|
];
|
|
|
|
let bulletins = vec![
|
|
Bulletin {
|
|
id: "1".to_string(),
|
|
title: "Weekly Bulletin".to_string(),
|
|
date: "2025-01-12T10:00:00Z".to_string(),
|
|
pdf_url: "https://example.com/bulletin.pdf".to_string(),
|
|
description: Some("This week's bulletin".to_string()),
|
|
thumbnail_url: None,
|
|
}
|
|
];
|
|
|
|
let verse = BibleVerse {
|
|
text: "For God so loved the world...".to_string(),
|
|
reference: "John 3:16".to_string(),
|
|
version: Some("KJV".to_string()),
|
|
};
|
|
|
|
let feed = aggregate_home_feed(&events, &sermons, &bulletins, Some(&verse));
|
|
|
|
assert!(feed.len() >= 4); // Should have events, sermons, bulletin, and verse
|
|
|
|
// Check that items are sorted by priority
|
|
for i in 1..feed.len() {
|
|
assert!(feed[i-1].priority >= feed[i].priority);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_media_type_display() {
|
|
assert_eq!(MediaType::Sermons.display_name(), "Sermons");
|
|
assert_eq!(MediaType::LiveStreams.display_name(), "Live Archives");
|
|
assert_eq!(MediaType::Sermons.icon_name(), "play.rectangle.fill");
|
|
assert_eq!(MediaType::LiveStreams.icon_name(), "dot.radiowaves.left.and.right");
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_media_content() {
|
|
let sermons = vec![
|
|
create_sample_sermon("1", "Regular Sermon"),
|
|
create_sample_sermon("2", "Livestream Service"),
|
|
create_sample_sermon("3", "Another Sermon"),
|
|
];
|
|
|
|
let regular_sermons = get_media_content(&sermons, &MediaType::Sermons);
|
|
assert_eq!(regular_sermons.len(), 2);
|
|
|
|
let livestreams = get_media_content(&sermons, &MediaType::LiveStreams);
|
|
assert_eq!(livestreams.len(), 1);
|
|
assert!(livestreams[0].title.contains("Livestream"));
|
|
}
|
|
} |