Skip to main content

scrobble_scrubber/
session_manager.rs

1use lastfm_edit::{
2    LastFmEditClientImpl, LastFmEditSession, Result as LastFmResult,
3    SessionManager as LastFmSessionManager,
4};
5
6/// Manages session persistence and validation using lastfm-edit's SessionManager
7pub struct SessionManager {
8    inner: LastFmSessionManager,
9    username: String,
10}
11
12impl SessionManager {
13    /// Create a new session manager for the given username
14    pub fn new(username: &str) -> Self {
15        let inner = LastFmSessionManager::new("scrobble-scrubber");
16        Self {
17            inner,
18            username: username.to_string(),
19        }
20    }
21
22    /// Try to load an existing session from disk
23    pub fn load_session(&self) -> Option<LastFmEditSession> {
24        match self.inner.load_session(&self.username) {
25            Ok(session) => {
26                log::info!("Loaded existing session for user: {}", session.username);
27                Some(session)
28            }
29            Err(e) => {
30                log::debug!("No existing session found: {e}");
31                None
32            }
33        }
34    }
35
36    /// Save a session to disk
37    pub fn save_session(&self, session: &LastFmEditSession) -> Result<(), std::io::Error> {
38        match self.inner.save_session(session) {
39            Ok(()) => {
40                log::info!("Session saved for user: {}", session.username);
41                Ok(())
42            }
43            Err(e) => {
44                log::warn!("Failed to save session: {e}");
45                Err(std::io::Error::other(format!(
46                    "Failed to save session: {e}"
47                )))
48            }
49        }
50    }
51
52    /// Validate if a session is still working by making a test request to settings page
53    pub async fn validate_session(&self, session: &LastFmEditSession) -> bool {
54        log::debug!("Validating existing session...");
55
56        match tokio::time::timeout(
57            std::time::Duration::from_secs(10),
58            self.check_session_validity(session),
59        )
60        .await
61        {
62            Ok(true) => {
63                log::info!("Session validation successful");
64                true
65            }
66            Ok(false) => {
67                log::warn!("Session validation failed - redirected to login");
68                false
69            }
70            Err(_) => {
71                log::warn!("Session validation timed out");
72                false
73            }
74        }
75    }
76
77    /// Test session validity by making a request to a protected settings page
78    async fn check_session_validity(&self, session: &LastFmEditSession) -> bool {
79        use reqwest::Client;
80
81        let client = Client::new();
82        let test_url = "https://www.last.fm/settings/subscription/automatic-edits/tracks";
83
84        // Build cookie header from session data
85        let cookie_header = session.cookies.join("; ");
86
87        let mut request_builder = client
88            .get(test_url)
89            .header("User-Agent", "Mozilla/5.0 (compatible; scrobble-scrubber)")
90            .header("Cookie", &cookie_header);
91
92        // Add CSRF token if available
93        if let Some(csrf_token) = &session.csrf_token {
94            request_builder = request_builder.header("X-CSRFToken", csrf_token);
95        }
96
97        match request_builder.send().await {
98            Ok(response) => {
99                let final_url = response.url().to_string();
100                log::debug!("Session validation response URL: {final_url}");
101
102                // If we're redirected to login, the session is invalid
103                // If we stay on the settings page, the session is valid
104                !final_url.contains("/login")
105            }
106            Err(e) => {
107                log::warn!("Session validation request failed: {e}");
108                false
109            }
110        }
111    }
112
113    /// Remove the saved session file
114    pub fn clear_session(&self) -> Result<(), std::io::Error> {
115        match self.inner.remove_session(&self.username) {
116            Ok(()) => {
117                log::info!("Cleared session for user: {}", self.username);
118                Ok(())
119            }
120            Err(e) => {
121                log::warn!("Failed to clear session: {e}");
122                Err(std::io::Error::other(format!(
123                    "Failed to clear session: {e}"
124                )))
125            }
126        }
127    }
128
129    /// Try to restore a working session, validating it if necessary
130    pub async fn try_restore_session(&self) -> Option<LastFmEditSession> {
131        let session = self.load_session()?;
132
133        // Always validate the session since lastfm-edit's SessionManager doesn't track staleness
134        log::info!("Validating loaded session...");
135
136        if self.validate_session(&session).await {
137            // Session is still valid, re-save it to update any metadata
138            if let Err(e) = self.save_session(&session) {
139                log::warn!("Failed to update session: {e}");
140            }
141            Some(session)
142        } else {
143            // Session is invalid, clear it
144            log::warn!("Session validation failed, clearing stored session");
145            let _ = self.clear_session();
146            None
147        }
148    }
149
150    /// Create and save a new session after successful login
151    pub async fn create_and_save_session(
152        &self,
153        username: &str,
154        password: &str,
155    ) -> LastFmResult<LastFmEditSession> {
156        log::info!("Creating new Last.fm session...");
157
158        let http_client = http_client::native::NativeClient::new();
159        let client =
160            LastFmEditClientImpl::login_with_credentials(Box::new(http_client), username, password)
161                .await?;
162
163        let session = client.get_session();
164
165        // Save the session
166        if let Err(e) = self.save_session(&session) {
167            log::warn!("Failed to save session: {e}");
168        }
169
170        Ok(session)
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test_log::test]
179    fn should_create_session_manager_with_username() {
180        let manager = SessionManager::new("testuser");
181        assert_eq!(manager.username, "testuser");
182    }
183}