Skip to main content

scrobble_scrubber/
recent_user_manager.rs

1use serde::{Deserialize, Serialize};
2use std::fs;
3use std::path::PathBuf;
4
5/// Manager for tracking the most recently used username
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct RecentUserData {
8    /// The most recently used username
9    pub username: String,
10    /// When this username was last used (Unix timestamp)
11    pub last_used: u64,
12    /// Version for future compatibility
13    pub version: u32,
14}
15
16impl RecentUserData {
17    /// Create new recent user data
18    pub fn new(username: String) -> Self {
19        Self {
20            username,
21            last_used: std::time::SystemTime::now()
22                .duration_since(std::time::UNIX_EPOCH)
23                .unwrap_or_default()
24                .as_secs(),
25            version: 1,
26        }
27    }
28
29    /// Update the last used timestamp
30    pub fn update_last_used(&mut self) {
31        self.last_used = std::time::SystemTime::now()
32            .duration_since(std::time::UNIX_EPOCH)
33            .unwrap_or_default()
34            .as_secs();
35    }
36}
37
38/// Manages tracking of the most recently used username
39pub struct RecentUserManager {
40    recent_user_file_path: PathBuf,
41}
42
43impl RecentUserManager {
44    /// Create a new recent user manager
45    pub fn new() -> Self {
46        let recent_user_file_path = Self::get_recent_user_file_path();
47        Self {
48            recent_user_file_path,
49        }
50    }
51
52    /// Get the recent user file path using XDG data directory
53    fn get_recent_user_file_path() -> PathBuf {
54        if let Some(data_dir) = dirs::data_dir() {
55            data_dir.join("scrobble-scrubber").join("recent_user.json")
56        } else {
57            // Fallback to current directory if XDG data directory is not available
58            PathBuf::from("recent_user.json")
59        }
60    }
61
62    /// Load the most recent user from disk
63    pub fn load_recent_user(&self) -> Option<RecentUserData> {
64        if !self.recent_user_file_path.exists() {
65            log::debug!(
66                "No recent user file found at: {}",
67                self.recent_user_file_path.display()
68            );
69            return None;
70        }
71
72        match fs::read_to_string(&self.recent_user_file_path) {
73            Ok(content) => {
74                match serde_json::from_str::<RecentUserData>(&content) {
75                    Ok(recent_user) => {
76                        log::info!("Loaded recent user: {}", recent_user.username);
77                        Some(recent_user)
78                    }
79                    Err(e) => {
80                        log::warn!("Failed to parse recent user file: {e}. Starting fresh.");
81                        // Remove corrupted file
82                        let _ = fs::remove_file(&self.recent_user_file_path);
83                        None
84                    }
85                }
86            }
87            Err(e) => {
88                log::warn!("Failed to read recent user file: {e}. Starting fresh.");
89                None
90            }
91        }
92    }
93
94    /// Save the most recent user to disk
95    pub fn save_recent_user(&self, username: &str) -> Result<(), std::io::Error> {
96        let recent_user = RecentUserData::new(username.to_string());
97
98        // Ensure the directory exists
99        if let Some(parent) = self.recent_user_file_path.parent() {
100            fs::create_dir_all(parent)?;
101        }
102
103        let content = serde_json::to_string_pretty(&recent_user)
104            .map_err(|e| std::io::Error::other(format!("Failed to serialize recent user: {e}")))?;
105
106        fs::write(&self.recent_user_file_path, content)?;
107        log::info!("Recent user saved: {username}");
108        Ok(())
109    }
110
111    /// Get the most recent username, if available
112    pub fn get_recent_username(&self) -> Option<String> {
113        self.load_recent_user().map(|data| data.username)
114    }
115
116    /// Update the recent user (or create if it doesn't exist)
117    pub fn update_recent_user(&self, username: &str) -> Result<(), std::io::Error> {
118        self.save_recent_user(username)
119    }
120
121    /// Clear the recent user file
122    pub fn clear_recent_user(&self) -> Result<(), std::io::Error> {
123        if self.recent_user_file_path.exists() {
124            fs::remove_file(&self.recent_user_file_path)?;
125            log::info!("Cleared recent user file");
126        }
127        Ok(())
128    }
129}
130
131impl Default for RecentUserManager {
132    fn default() -> Self {
133        Self::new()
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test_log::test]
142    fn should_create_recent_user_data_with_current_timestamp() {
143        let user_data = RecentUserData::new("testuser".to_string());
144        assert_eq!(user_data.username, "testuser");
145        assert_eq!(user_data.version, 1);
146
147        // Should have a reasonable timestamp
148        let now = std::time::SystemTime::now()
149            .duration_since(std::time::UNIX_EPOCH)
150            .unwrap_or_default()
151            .as_secs();
152
153        // Should be within a few seconds
154        assert!(user_data.last_used <= now && user_data.last_used >= (now - 10));
155    }
156
157    #[test_log::test]
158    fn should_update_last_used_timestamp() {
159        let mut user_data = RecentUserData::new("testuser".to_string());
160        let original_time = user_data.last_used;
161
162        // Sleep long enough to ensure time difference in seconds resolution
163        std::thread::sleep(std::time::Duration::from_secs(1));
164
165        user_data.update_last_used();
166        assert!(user_data.last_used > original_time);
167    }
168}