jellytau_lib/commands/download/
smart_cache.rs1use 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#[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#[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 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 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#[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#[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#[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#[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#[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 let cache = {
131 let guard = smart_cache.0.lock().map_err(|e| e.to_string())?;
132 guard.clone()
133 };
134
135 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 let should_download = cache.should_cache_album(&album_id).unwrap_or(false);
158
159 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 recommendations.sort_by_key(|r| std::cmp::Reverse(r.tracks_played));
191
192 Ok(recommendations)
193}
194
195#[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 let threshold = cache
206 .get_config()
207 .map(|c| c.album_affinity_threshold)
208 .unwrap_or(3);
209
210 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 statuses.sort_by_key(|s| std::cmp::Reverse(s.unique_tracks_played));
228
229 Ok(statuses)
230}