//! Tauri commands for offline data access use serde::{Deserialize, Serialize}; use std::sync::Arc; use tauri::State; use super::DatabaseWrapper; use crate::storage::db_service::{DatabaseService, Query, QueryParam}; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct OfflineItem { pub id: String, pub name: String, pub item_type: String, pub album_id: Option, pub album_name: Option, pub artists: Option, pub runtime_ticks: Option, pub primary_image_tag: Option, } /// Check if an item is available offline #[tauri::command] pub async fn offline_is_available( db: State<'_, DatabaseWrapper>, item_id: String, ) -> Result { let db_service = { let database = db.0.lock().map_err(|e| e.to_string())?; Arc::new(database.service()) }; let query = Query::with_params( "SELECT COUNT(*) FROM downloads WHERE item_id = ? AND status = 'completed'", vec![QueryParam::String(item_id)], ); let count: i64 = db_service .query_one(query, |row| row.get(0)) .await .map_err(|e| e.to_string())?; Ok(count > 0) } /// Get all offline items for a user #[tauri::command] pub async fn offline_get_items( db: State<'_, DatabaseWrapper>, user_id: String, ) -> Result, String> { let db_service = { let database = db.0.lock().map_err(|e| e.to_string())?; Arc::new(database.service()) }; let query = Query::with_params( "SELECT i.id, i.name, i.item_type, i.album_id, i.album_name, i.artists, i.runtime_ticks, i.primary_image_tag FROM items i INNER JOIN downloads d ON i.id = d.item_id WHERE d.user_id = ? AND d.status = 'completed' ORDER BY d.completed_at DESC", vec![QueryParam::String(user_id)], ); db_service .query_many(query, |row| { Ok(OfflineItem { id: row.get(0)?, name: row.get(1)?, item_type: row.get(2)?, album_id: row.get(3)?, album_name: row.get(4)?, artists: row.get(5)?, runtime_ticks: row.get(6)?, primary_image_tag: row.get(7)?, }) }) .await .map_err(|e| e.to_string()) } /// Search offline items #[tauri::command] pub async fn offline_search( db: State<'_, DatabaseWrapper>, user_id: String, query: String, ) -> Result, String> { let db_service = { let database = db.0.lock().map_err(|e| e.to_string())?; Arc::new(database.service()) }; let search_query = format!("%{}%", query.to_lowercase()); let db_query = Query::with_params( "SELECT i.id, i.name, i.item_type, i.album_id, i.album_name, i.artists, i.runtime_ticks, i.primary_image_tag FROM items i INNER JOIN downloads d ON i.id = d.item_id WHERE d.user_id = ? AND d.status = 'completed' AND (LOWER(i.name) LIKE ? OR LOWER(i.artists) LIKE ? OR LOWER(i.album_name) LIKE ?) ORDER BY i.name LIMIT 50", vec![ QueryParam::String(user_id), QueryParam::String(search_query.clone()), QueryParam::String(search_query.clone()), QueryParam::String(search_query), ], ); db_service .query_many(db_query, |row| { Ok(OfflineItem { id: row.get(0)?, name: row.get(1)?, item_type: row.get(2)?, album_id: row.get(3)?, album_name: row.get(4)?, artists: row.get(5)?, runtime_ticks: row.get(6)?, primary_image_tag: row.get(7)?, }) }) .await .map_err(|e| e.to_string()) } // TRACES: UR-002, UR-011 | DR-017 | UT-044 #[cfg(test)] mod tests { use super::*; #[test] fn test_offline_item_serialization() { let item = OfflineItem { id: "123".to_string(), name: "Test Song".to_string(), item_type: "Audio".to_string(), album_id: Some("album1".to_string()), album_name: Some("Test Album".to_string()), artists: Some("Artist 1".to_string()), runtime_ticks: Some(180000000), primary_image_tag: Some("tag123".to_string()), }; let json = serde_json::to_string(&item).unwrap(); assert!(json.contains("\"itemType\":\"Audio\"")); assert!(json.contains("\"albumName\":\"Test Album\"")); } }