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}