Skip to main content

jellytau_lib/commands/download/
smart_cache.rs

1//! Smart-cache statistics/config and album recommendation commands.
2//!
3//! TRACES: UR-045 | DR-057
4
5use log::info;
6use std::sync::Arc;
7use tauri::State;
8
9use crate::commands::{DatabaseWrapper, SmartCacheWrapper};
10use crate::storage::db_service::{DatabaseService, Query, QueryParam};
11
12/// SmartCache statistics
13#[derive(specta::Type, Debug, Clone, serde::Serialize)]
14pub struct SmartCacheStats {
15    pub total_size: u64,
16    pub storage_limit: u64,
17    pub available_space: u64,
18    pub items_count: i64,
19    pub config: crate::download::cache::CacheConfig,
20}
21
22/// Get SmartCache statistics
23#[tauri::command]
24#[specta::specta]
25pub async fn get_smart_cache_stats(
26    db: State<'_, DatabaseWrapper>,
27    smart_cache: State<'_, SmartCacheWrapper>,
28    user_id: String,
29) -> Result<SmartCacheStats, String> {
30    let db_service = {
31        let database = db.0.lock().map_err(|e| e.to_string())?;
32        Arc::new(database.service())
33    };
34
35    // Clone cache to avoid holding lock across async operations
36    let cache = {
37        let guard = smart_cache.0.lock().map_err(|e| e.to_string())?;
38        guard.clone()
39    };
40
41    let total_size = cache
42        .get_total_download_size_async(&db_service, &user_id)
43        .await?;
44
45    let config = cache
46        .get_config()
47        .ok_or_else(|| "Failed to get cache config".to_string())?;
48
49    let storage_limit = config.storage_limit;
50    let available_space = storage_limit.saturating_sub(total_size);
51
52    // Get item count
53    let count_query = Query::with_params(
54        "SELECT COUNT(*) FROM downloads WHERE user_id = ? AND status = 'completed'",
55        vec![QueryParam::String(user_id)],
56    );
57    let items_count: i64 = db_service
58        .query_one(count_query, |row| row.get(0))
59        .await
60        .unwrap_or(0);
61
62    Ok(SmartCacheStats {
63        total_size,
64        storage_limit,
65        available_space,
66        items_count,
67        config,
68    })
69}
70
71/// Update SmartCache configuration
72#[tauri::command]
73#[specta::specta]
74pub async fn update_smart_cache_config(
75    smart_cache: State<'_, SmartCacheWrapper>,
76    config: crate::download::cache::CacheConfig,
77) -> Result<(), String> {
78    let cache = smart_cache.0.lock().map_err(|e| e.to_string())?;
79    cache.update_config(config);
80    info!("Updated SmartCache configuration");
81    Ok(())
82}
83
84/// Get SmartCache configuration
85#[tauri::command]
86#[specta::specta]
87pub async fn get_smart_cache_config(
88    smart_cache: State<'_, SmartCacheWrapper>,
89) -> Result<crate::download::cache::CacheConfig, String> {
90    let cache = smart_cache.0.lock().map_err(|e| e.to_string())?;
91    cache
92        .get_config()
93        .ok_or_else(|| "Failed to get cache config".to_string())
94}
95
96/// Album recommendation info
97#[derive(specta::Type, Debug, Clone, serde::Serialize)]
98pub struct AlbumRecommendation {
99    pub album_id: String,
100    pub album_name: String,
101    pub tracks_played: usize,
102    pub total_tracks: usize,
103    pub should_download: bool,
104}
105
106/// Album affinity status info
107#[derive(specta::Type, Debug, Clone, serde::Serialize)]
108#[serde(rename_all = "camelCase")]
109pub struct AlbumAffinityStatus {
110    pub album_id: String,
111    pub unique_tracks_played: usize,
112    pub threshold: usize,
113    pub threshold_reached: bool,
114}
115
116/// Get album recommendations based on play history
117#[tauri::command]
118#[specta::specta]
119pub async fn get_album_recommendations(
120    db: State<'_, DatabaseWrapper>,
121    smart_cache: State<'_, SmartCacheWrapper>,
122    user_id: String,
123) -> Result<Vec<AlbumRecommendation>, String> {
124    let db_service = {
125        let database = db.0.lock().map_err(|e| e.to_string())?;
126        Arc::new(database.service())
127    };
128
129    // Clone cache to avoid holding lock across async operations
130    let cache = {
131        let guard = smart_cache.0.lock().map_err(|e| e.to_string())?;
132        guard.clone()
133    };
134
135    // Get all albums that user has played tracks from
136    let query = Query::with_params(
137        "SELECT DISTINCT i.album_id, a.name
138         FROM user_data ud
139         JOIN items i ON ud.item_id = i.id
140         JOIN items a ON i.album_id = a.id
141         WHERE ud.user_id = ?
142           AND ud.play_count > 0
143           AND i.item_type = 'Audio'
144           AND i.album_id IS NOT NULL",
145        vec![QueryParam::String(user_id.clone())],
146    );
147
148    let albums: Vec<(String, String)> = db_service
149        .query_many(query, |row| Ok((row.get(0)?, row.get(1)?)))
150        .await
151        .unwrap_or_default();
152
153    let mut recommendations = Vec::new();
154
155    for (album_id, album_name) in albums {
156        // Check if should cache
157        let should_download = cache.should_cache_album(&album_id).unwrap_or(false);
158
159        // Get track counts
160        let tracks_query = Query::with_params(
161            "SELECT
162                COUNT(*) as total,
163                COUNT(ud.id) as played
164             FROM items i
165             LEFT JOIN user_data ud ON i.id = ud.item_id AND ud.user_id = ?
166             WHERE i.album_id = ? AND i.item_type = 'Audio'",
167            vec![
168                QueryParam::String(user_id.clone()),
169                QueryParam::String(album_id.clone()),
170            ],
171        );
172
173        let (total_tracks, tracks_played): (i64, i64) = db_service
174            .query_one(tracks_query, |row| Ok((row.get(0)?, row.get(1)?)))
175            .await
176            .unwrap_or((0, 0));
177
178        if tracks_played > 0 {
179            recommendations.push(AlbumRecommendation {
180                album_id,
181                album_name,
182                tracks_played: tracks_played as usize,
183                total_tracks: total_tracks as usize,
184                should_download,
185            });
186        }
187    }
188
189    // Sort by tracks played (descending)
190    recommendations.sort_by_key(|r| std::cmp::Reverse(r.tracks_played));
191
192    Ok(recommendations)
193}
194
195/// Get album affinity status for all tracked albums
196/// This shows the SmartCache's internal play history and threshold status
197#[tauri::command]
198#[specta::specta]
199pub fn get_album_affinity_status(
200    smart_cache: State<'_, SmartCacheWrapper>,
201) -> Result<Vec<AlbumAffinityStatus>, String> {
202    let cache = smart_cache.0.lock().map_err(|e| e.to_string())?;
203
204    // Get the threshold from config
205    let threshold = cache
206        .get_config()
207        .map(|c| c.album_affinity_threshold)
208        .unwrap_or(3);
209
210    // Get all tracked albums with their play counts
211    let play_history = cache.get_album_play_history();
212
213    let mut statuses: Vec<AlbumAffinityStatus> = play_history
214        .into_iter()
215        .map(|(album_id, unique_tracks_played)| {
216            let threshold_reached = unique_tracks_played >= threshold;
217            AlbumAffinityStatus {
218                album_id,
219                unique_tracks_played,
220                threshold,
221                threshold_reached,
222            }
223        })
224        .collect();
225
226    // Sort by play count (descending)
227    statuses.sort_by_key(|s| std::cmp::Reverse(s.unique_tracks_played));
228
229    Ok(statuses)
230}