//! Smart-cache statistics/config and album recommendation commands. //! //! TRACES: UR-045 | DR-057 use log::info; use std::sync::Arc; use tauri::State; use crate::commands::{DatabaseWrapper, SmartCacheWrapper}; use crate::storage::db_service::{DatabaseService, Query, QueryParam}; /// SmartCache statistics #[derive(specta::Type, Debug, Clone, serde::Serialize)] pub struct SmartCacheStats { pub total_size: u64, pub storage_limit: u64, pub available_space: u64, pub items_count: i64, pub config: crate::download::cache::CacheConfig, } /// Get SmartCache statistics #[tauri::command] #[specta::specta] pub async fn get_smart_cache_stats( db: State<'_, DatabaseWrapper>, smart_cache: State<'_, SmartCacheWrapper>, user_id: String, ) -> Result { let db_service = { let database = db.0.lock().map_err(|e| e.to_string())?; Arc::new(database.service()) }; // Clone cache to avoid holding lock across async operations let cache = { let guard = smart_cache.0.lock().map_err(|e| e.to_string())?; guard.clone() }; let total_size = cache .get_total_download_size_async(&db_service, &user_id) .await?; let config = cache .get_config() .ok_or_else(|| "Failed to get cache config".to_string())?; let storage_limit = config.storage_limit; let available_space = storage_limit.saturating_sub(total_size); // Get item count let count_query = Query::with_params( "SELECT COUNT(*) FROM downloads WHERE user_id = ? AND status = 'completed'", vec![QueryParam::String(user_id)], ); let items_count: i64 = db_service .query_one(count_query, |row| row.get(0)) .await .unwrap_or(0); Ok(SmartCacheStats { total_size, storage_limit, available_space, items_count, config, }) } /// Update SmartCache configuration #[tauri::command] #[specta::specta] pub async fn update_smart_cache_config( smart_cache: State<'_, SmartCacheWrapper>, config: crate::download::cache::CacheConfig, ) -> Result<(), String> { let cache = smart_cache.0.lock().map_err(|e| e.to_string())?; cache.update_config(config); info!("Updated SmartCache configuration"); Ok(()) } /// Get SmartCache configuration #[tauri::command] #[specta::specta] pub async fn get_smart_cache_config( smart_cache: State<'_, SmartCacheWrapper>, ) -> Result { let cache = smart_cache.0.lock().map_err(|e| e.to_string())?; cache .get_config() .ok_or_else(|| "Failed to get cache config".to_string()) } /// Album recommendation info #[derive(specta::Type, Debug, Clone, serde::Serialize)] pub struct AlbumRecommendation { pub album_id: String, pub album_name: String, pub tracks_played: usize, pub total_tracks: usize, pub should_download: bool, } /// Album affinity status info #[derive(specta::Type, Debug, Clone, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct AlbumAffinityStatus { pub album_id: String, pub unique_tracks_played: usize, pub threshold: usize, pub threshold_reached: bool, } /// Get album recommendations based on play history #[tauri::command] #[specta::specta] pub async fn get_album_recommendations( db: State<'_, DatabaseWrapper>, smart_cache: State<'_, SmartCacheWrapper>, user_id: String, ) -> Result, String> { let db_service = { let database = db.0.lock().map_err(|e| e.to_string())?; Arc::new(database.service()) }; // Clone cache to avoid holding lock across async operations let cache = { let guard = smart_cache.0.lock().map_err(|e| e.to_string())?; guard.clone() }; // Get all albums that user has played tracks from let query = Query::with_params( "SELECT DISTINCT i.album_id, a.name FROM user_data ud JOIN items i ON ud.item_id = i.id JOIN items a ON i.album_id = a.id WHERE ud.user_id = ? AND ud.play_count > 0 AND i.item_type = 'Audio' AND i.album_id IS NOT NULL", vec![QueryParam::String(user_id.clone())], ); let albums: Vec<(String, String)> = db_service .query_many(query, |row| Ok((row.get(0)?, row.get(1)?))) .await .unwrap_or_default(); let mut recommendations = Vec::new(); for (album_id, album_name) in albums { // Check if should cache let should_download = cache.should_cache_album(&album_id).unwrap_or(false); // Get track counts let tracks_query = Query::with_params( "SELECT COUNT(*) as total, COUNT(ud.id) as played FROM items i LEFT JOIN user_data ud ON i.id = ud.item_id AND ud.user_id = ? WHERE i.album_id = ? AND i.item_type = 'Audio'", vec![ QueryParam::String(user_id.clone()), QueryParam::String(album_id.clone()), ], ); let (total_tracks, tracks_played): (i64, i64) = db_service .query_one(tracks_query, |row| Ok((row.get(0)?, row.get(1)?))) .await .unwrap_or((0, 0)); if tracks_played > 0 { recommendations.push(AlbumRecommendation { album_id, album_name, tracks_played: tracks_played as usize, total_tracks: total_tracks as usize, should_download, }); } } // Sort by tracks played (descending) recommendations.sort_by(|a, b| b.tracks_played.cmp(&a.tracks_played)); Ok(recommendations) } /// Get album affinity status for all tracked albums /// This shows the SmartCache's internal play history and threshold status #[tauri::command] #[specta::specta] pub fn get_album_affinity_status( smart_cache: State<'_, SmartCacheWrapper>, ) -> Result, String> { let cache = smart_cache.0.lock().map_err(|e| e.to_string())?; // Get the threshold from config let threshold = cache .get_config() .map(|c| c.album_affinity_threshold) .unwrap_or(3); // Get all tracked albums with their play counts let play_history = cache.get_album_play_history(); let mut statuses: Vec = play_history .into_iter() .map(|(album_id, unique_tracks_played)| { let threshold_reached = unique_tracks_played >= threshold; AlbumAffinityStatus { album_id, unique_tracks_played, threshold, threshold_reached, } }) .collect(); // Sort by play count (descending) statuses.sort_by(|a, b| b.unique_tracks_played.cmp(&a.unique_tracks_played)); Ok(statuses) }