use crate::error::{RecommendError, RecommendResult};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserProfile {
pub user_id: Uuid,
pub preferred_categories: HashMap<String, f32>,
pub disliked_categories: HashMap<String, f32>,
pub avg_watch_duration_ms: i64,
pub avg_completion_rate: f32,
pub viewing_patterns: ViewingPatterns,
pub content_preferences: ContentPreferences,
pub engagement_level: f32,
pub created_at: i64,
pub updated_at: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ViewingPatterns {
pub preferred_hours: Vec<u8>,
pub preferred_days: Vec<u8>,
pub avg_session_duration_min: u32,
pub binge_tendency: f32,
}
impl Default for ViewingPatterns {
fn default() -> Self {
Self {
preferred_hours: Vec::new(),
preferred_days: Vec::new(),
avg_session_duration_min: 30,
binge_tendency: 0.5,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ContentPreferences {
pub content_types: HashMap<String, f32>,
pub duration_preference: Option<(u32, u32)>,
pub quality_preference: Option<String>,
pub language_preferences: Vec<String>,
}
impl UserProfile {
#[must_use]
pub fn new(user_id: Uuid) -> Self {
let now = chrono::Utc::now().timestamp();
Self {
user_id,
preferred_categories: HashMap::new(),
disliked_categories: HashMap::new(),
avg_watch_duration_ms: 0,
avg_completion_rate: 0.0,
viewing_patterns: ViewingPatterns::default(),
content_preferences: ContentPreferences::default(),
engagement_level: 0.5,
created_at: now,
updated_at: now,
}
}
pub fn update_category_preference(&mut self, category: String, weight: f32) {
if weight > 0.0 {
*self.preferred_categories.entry(category).or_insert(0.0) += weight;
} else {
*self.disliked_categories.entry(category).or_insert(0.0) += weight.abs();
}
self.updated_at = chrono::Utc::now().timestamp();
}
#[must_use]
pub fn get_category_score(&self, category: &str) -> f32 {
let preference = self.preferred_categories.get(category).unwrap_or(&0.0);
let dislike = self.disliked_categories.get(category).unwrap_or(&0.0);
preference - dislike
}
pub fn update_from_view(&mut self, watch_time_ms: i64, completion_rate: f32) {
let alpha = 0.1;
self.avg_watch_duration_ms = (alpha * watch_time_ms as f32
+ (1.0 - alpha) * self.avg_watch_duration_ms as f32)
as i64;
self.avg_completion_rate =
alpha * completion_rate + (1.0 - alpha) * self.avg_completion_rate;
self.engagement_level = completion_rate.max(self.engagement_level * 0.9);
self.updated_at = chrono::Utc::now().timestamp();
}
#[must_use]
pub fn get_top_categories(&self, limit: usize) -> Vec<(String, f32)> {
let mut categories: Vec<(String, f32)> = self
.preferred_categories
.iter()
.map(|(k, v)| (k.clone(), *v))
.collect();
categories.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
categories.truncate(limit);
categories
}
}
pub struct UserProfileManager {
profiles: HashMap<Uuid, UserProfile>,
}
impl UserProfileManager {
#[must_use]
pub fn new() -> Self {
Self {
profiles: HashMap::new(),
}
}
pub fn get_or_create_profile(&mut self, user_id: Uuid) -> &mut UserProfile {
self.profiles
.entry(user_id)
.or_insert_with(|| UserProfile::new(user_id))
}
pub fn get_profile(&self, user_id: Uuid) -> RecommendResult<UserProfile> {
self.profiles
.get(&user_id)
.cloned()
.ok_or(RecommendError::UserNotFound(user_id))
}
pub fn update_from_view(
&mut self,
user_id: Uuid,
_content_id: Uuid,
watch_time_ms: i64,
completed: bool,
) -> RecommendResult<()> {
let profile = self.get_or_create_profile(user_id);
let completion_rate = if completed { 1.0 } else { 0.5 };
profile.update_from_view(watch_time_ms, completion_rate);
Ok(())
}
pub fn update_from_rating(
&mut self,
user_id: Uuid,
_content_id: Uuid,
rating: f32,
) -> RecommendResult<()> {
let profile = self.get_or_create_profile(user_id);
let engagement_boost = (rating / 5.0) * 0.1;
profile.engagement_level = (profile.engagement_level + engagement_boost).min(1.0);
Ok(())
}
pub fn get_similar_users(&self, user_id: Uuid, limit: usize) -> RecommendResult<Vec<Uuid>> {
let profile = self
.profiles
.get(&user_id)
.ok_or(RecommendError::UserNotFound(user_id))?;
let mut similarities: Vec<(Uuid, f32)> = self
.profiles
.iter()
.filter(|(id, _)| **id != user_id)
.map(|(id, other_profile)| (*id, calculate_profile_similarity(profile, other_profile)))
.collect();
similarities.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
similarities.truncate(limit);
Ok(similarities.into_iter().map(|(id, _)| id).collect())
}
}
impl Default for UserProfileManager {
fn default() -> Self {
Self::new()
}
}
fn calculate_profile_similarity(profile_a: &UserProfile, profile_b: &UserProfile) -> f32 {
let categories_a: std::collections::HashSet<_> =
profile_a.preferred_categories.keys().collect();
let categories_b: std::collections::HashSet<_> =
profile_b.preferred_categories.keys().collect();
let intersection = categories_a.intersection(&categories_b).count();
let union = categories_a.union(&categories_b).count();
if union == 0 {
return 0.0;
}
let category_sim = intersection as f32 / union as f32;
let engagement_diff = (profile_a.engagement_level - profile_b.engagement_level).abs();
let engagement_sim = 1.0 - engagement_diff;
0.7 * category_sim + 0.3 * engagement_sim
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_user_profile_creation() {
let user_id = Uuid::new_v4();
let profile = UserProfile::new(user_id);
assert_eq!(profile.user_id, user_id);
assert!(profile.preferred_categories.is_empty());
}
#[test]
fn test_update_category_preference() {
let mut profile = UserProfile::new(Uuid::new_v4());
profile.update_category_preference(String::from("Action"), 1.0);
assert_eq!(profile.get_category_score("Action"), 1.0);
}
#[test]
fn test_update_from_view() {
let mut profile = UserProfile::new(Uuid::new_v4());
profile.update_from_view(60000, 0.8);
assert!(profile.avg_watch_duration_ms > 0);
assert!(profile.avg_completion_rate > 0.0);
}
#[test]
fn test_get_top_categories() {
let mut profile = UserProfile::new(Uuid::new_v4());
profile.update_category_preference(String::from("Action"), 5.0);
profile.update_category_preference(String::from("Drama"), 3.0);
profile.update_category_preference(String::from("Comedy"), 4.0);
let top = profile.get_top_categories(2);
assert_eq!(top.len(), 2);
assert_eq!(top[0].0, "Action");
}
#[test]
fn test_profile_manager() {
let mut manager = UserProfileManager::new();
let user_id = Uuid::new_v4();
let profile = manager.get_or_create_profile(user_id);
assert_eq!(profile.user_id, user_id);
}
}