Skip to main content

jellytau_lib/commands/
device.rs

1//! Device identification commands
2//!
3//! Handles persistent device ID generation and retrieval for Jellyfin server communication.
4//! TRACES: UR-009 | DR-011
5
6use std::sync::Arc;
7
8use log::info;
9use tauri::State;
10use uuid::Uuid;
11
12use crate::commands::storage::DatabaseWrapper;
13use crate::storage::db_service::{DatabaseService, Query, QueryParam};
14
15/// Get or create the device ID.
16/// Device ID is a UUID v4 that persists across app restarts.
17/// On first call, generates and stores a new UUID.
18/// On subsequent calls, retrieves the stored UUID.
19///
20/// # Returns
21/// - `Ok(String)` - The device ID (UUID v4)
22/// - `Err(String)` - If database operation fails
23///
24/// TRACES: UR-009 | DR-011
25#[tauri::command]
26#[specta::specta]
27pub async fn device_get_id(db: State<'_, DatabaseWrapper>) -> Result<String, String> {
28    let db_service = {
29        let database = db.0.lock().map_err(|e| e.to_string())?;
30        Arc::new(database.service())
31    };
32
33    // Try to get existing device ID from database
34    let query = Query::with_params(
35        "SELECT value FROM app_settings WHERE key = ?",
36        vec![QueryParam::String("device_id".to_string())],
37    );
38
39    let existing_id: Option<String> = db_service
40        .query_one(query, |row| row.get(0))
41        .await
42        .ok()
43        .flatten();
44
45    if let Some(device_id) = existing_id {
46        info!("[Device] Retrieved existing device ID");
47        return Ok(device_id);
48    }
49
50    // Generate new device ID
51    let device_id = Uuid::new_v4().to_string();
52
53    // Store it in database
54    let insert_query = Query::with_params(
55        "INSERT INTO app_settings (key, value) VALUES (?, ?)",
56        vec![
57            QueryParam::String("device_id".to_string()),
58            QueryParam::String(device_id.clone()),
59        ],
60    );
61
62    db_service
63        .execute(insert_query)
64        .await
65        .map_err(|e| e.to_string())?;
66
67    info!("[Device] Generated and stored new device ID");
68    Ok(device_id)
69}
70
71/// Set the device ID (primarily for testing or recovery).
72/// Overwrites any existing device ID.
73///
74/// # Arguments
75/// * `device_id` - The device ID to store (should be UUID v4 format)
76///
77/// # Returns
78/// - `Ok(())` - If device ID was stored successfully
79/// - `Err(String)` - If database operation fails
80///
81/// TRACES: UR-009 | DR-011
82#[tauri::command]
83#[specta::specta]
84pub async fn device_set_id(
85    device_id: String,
86    db: State<'_, DatabaseWrapper>,
87) -> Result<(), String> {
88    let db_service = {
89        let database = db.0.lock().map_err(|e| e.to_string())?;
90        Arc::new(database.service())
91    };
92
93    let query = Query::with_params(
94        "INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)",
95        vec![
96            QueryParam::String("device_id".to_string()),
97            QueryParam::String(device_id),
98        ],
99    );
100
101    db_service.execute(query).await.map_err(|e| e.to_string())?;
102
103    info!("[Device] Device ID set");
104    Ok(())
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_device_id_is_valid_uuid() {
113        let id = Uuid::new_v4().to_string();
114        // Should parse as UUID
115        let parsed = Uuid::parse_str(&id);
116        assert!(parsed.is_ok(), "Device ID should be a valid UUID");
117    }
118
119    #[test]
120    fn test_device_id_format() {
121        let id = Uuid::new_v4().to_string();
122        // UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx (36 chars with hyphens)
123        assert_eq!(id.len(), 36, "Device ID should be 36 characters");
124        assert!(id.contains('-'), "Device ID should contain hyphens");
125    }
126
127    #[test]
128    fn test_device_ids_are_unique() {
129        let id1 = Uuid::new_v4().to_string();
130        let id2 = Uuid::new_v4().to_string();
131        assert_ne!(id1, id2, "Generated device IDs should be unique");
132    }
133}