//! Device identification commands //! //! Handles persistent device ID generation and retrieval for Jellyfin server communication. //! TRACES: UR-009 | DR-011 use std::sync::Arc; use log::info; use tauri::State; use uuid::Uuid; use crate::commands::storage::DatabaseWrapper; use crate::storage::db_service::{DatabaseService, Query, QueryParam}; /// Get or create the device ID. /// Device ID is a UUID v4 that persists across app restarts. /// On first call, generates and stores a new UUID. /// On subsequent calls, retrieves the stored UUID. /// /// # Returns /// - `Ok(String)` - The device ID (UUID v4) /// - `Err(String)` - If database operation fails /// /// TRACES: UR-009 | DR-011 #[tauri::command] #[specta::specta] pub async fn device_get_id(db: State<'_, DatabaseWrapper>) -> Result { let db_service = { let database = db.0.lock().map_err(|e| e.to_string())?; Arc::new(database.service()) }; // Try to get existing device ID from database let query = Query::with_params( "SELECT value FROM app_settings WHERE key = ?", vec![QueryParam::String("device_id".to_string())], ); let existing_id: Option = db_service .query_one(query, |row| row.get(0)) .await .ok() .flatten(); if let Some(device_id) = existing_id { info!("[Device] Retrieved existing device ID"); return Ok(device_id); } // Generate new device ID let device_id = Uuid::new_v4().to_string(); // Store it in database let insert_query = Query::with_params( "INSERT INTO app_settings (key, value) VALUES (?, ?)", vec![ QueryParam::String("device_id".to_string()), QueryParam::String(device_id.clone()), ], ); db_service .execute(insert_query) .await .map_err(|e| e.to_string())?; info!("[Device] Generated and stored new device ID"); Ok(device_id) } /// Set the device ID (primarily for testing or recovery). /// Overwrites any existing device ID. /// /// # Arguments /// * `device_id` - The device ID to store (should be UUID v4 format) /// /// # Returns /// - `Ok(())` - If device ID was stored successfully /// - `Err(String)` - If database operation fails /// /// TRACES: UR-009 | DR-011 #[tauri::command] #[specta::specta] pub async fn device_set_id( device_id: String, db: State<'_, DatabaseWrapper>, ) -> 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( "INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)", vec![ QueryParam::String("device_id".to_string()), QueryParam::String(device_id), ], ); db_service.execute(query).await.map_err(|e| e.to_string())?; info!("[Device] Device ID set"); Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_device_id_is_valid_uuid() { let id = Uuid::new_v4().to_string(); // Should parse as UUID let parsed = Uuid::parse_str(&id); assert!(parsed.is_ok(), "Device ID should be a valid UUID"); } #[test] fn test_device_id_format() { let id = Uuid::new_v4().to_string(); // UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx (36 chars with hyphens) assert_eq!(id.len(), 36, "Device ID should be 36 characters"); assert!(id.contains('-'), "Device ID should contain hyphens"); } #[test] fn test_device_ids_are_unique() { let id1 = Uuid::new_v4().to_string(); let id2 = Uuid::new_v4().to_string(); assert_ne!(id1, id2, "Generated device IDs should be unique"); } }