1use crate::types::{LastFmEditSession, LastFmError};
2use crate::Result;
3use http_client::{HttpClient, Request};
4use http_types::{Method, Url};
5use scraper::{Html, Selector};
6use std::collections::HashMap;
7use std::sync::Arc;
8
9pub struct LoginManager {
11 client: Arc<dyn HttpClient + Send + Sync>,
12 base_url: String,
13}
14
15impl LoginManager {
16 pub fn new(client: Arc<dyn HttpClient + Send + Sync>, base_url: String) -> Self {
17 Self { client, base_url }
18 }
19
20 pub async fn login(&self, username: &str, password: &str) -> Result<LastFmEditSession> {
37 log::info!("🔐 Starting Last.fm login for username: {username}");
38
39 let login_url = format!("{}/login", self.base_url);
41 let (csrf_token, next_field, mut cookies) = self.fetch_login_page(&login_url).await?;
42
43 let response = self
45 .submit_login_form(
46 &login_url,
47 username,
48 password,
49 &csrf_token,
50 &next_field,
51 &cookies,
52 )
53 .await?;
54
55 extract_cookies_from_response(&response, &mut cookies);
57 log::debug!("🍪 Cookies after login response: {cookies:?}");
58
59 self.validate_login_response(response, username, cookies, csrf_token)
61 .await
62 }
63
64 async fn fetch_login_page(
66 &self,
67 login_url: &str,
68 ) -> Result<(String, Option<String>, Vec<String>)> {
69 log::debug!("📡 Fetching login page: {login_url}");
70 let mut response = self.get(login_url).await?;
71
72 log::debug!("📋 Login page response status: {}", response.status());
73 log::debug!(
74 "📋 Login page response headers: {:?}",
75 response.iter().collect::<Vec<_>>()
76 );
77
78 let mut cookies = Vec::new();
80 extract_cookies_from_response(&response, &mut cookies);
81 log::debug!("🍪 Initial cookies from login page: {cookies:?}");
82
83 let html = response
85 .body_string()
86 .await
87 .map_err(|e| LastFmError::Http(e.to_string()))?;
88
89 log::debug!("📄 Login page HTML length: {} chars", html.len());
90 if html.len() < 500 {
91 log::debug!("📄 Login page HTML content (short): {html}");
92 }
93
94 let (csrf_token, next_field) = self.extract_login_form_data(&html)?;
96 log::debug!("🔑 Extracted CSRF token: {csrf_token}",);
97 log::debug!("➡️ Next field: {next_field:?}");
98
99 Ok((csrf_token, next_field, cookies))
100 }
101
102 async fn submit_login_form(
104 &self,
105 login_url: &str,
106 username: &str,
107 password: &str,
108 csrf_token: &str,
109 next_field: &Option<String>,
110 cookies: &[String],
111 ) -> Result<http_types::Response> {
112 let mut form_data = HashMap::new();
114 form_data.insert("csrfmiddlewaretoken", csrf_token);
115 form_data.insert("username_or_email", username);
116 form_data.insert("password", password);
117
118 if let Some(ref next_value) = next_field {
119 form_data.insert("next", next_value);
120 log::debug!("➡️ Including next field in form: {next_value}");
121 }
122
123 log::debug!(
124 "📝 Form data fields: {:?}",
125 form_data.keys().collect::<Vec<_>>()
126 );
127 log::debug!("📝 Form username: {username}");
128 log::debug!("📝 Form password length: {} chars", password.len());
129
130 let mut request = self.create_login_request(login_url, cookies)?;
132
133 let form_string: String = form_data
135 .iter()
136 .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
137 .collect::<Vec<_>>()
138 .join("&");
139
140 log::debug!("📤 Sending POST request to: {login_url}");
141 log::debug!("📤 Form body length: {} chars", form_string.len());
142 log::debug!("📤 Form body (masked): {form_string}");
143 log::debug!("📤 Request headers: Referer={}, Origin={}, Content-Type=application/x-www-form-urlencoded",
144 login_url, &self.base_url);
145
146 request.set_body(form_string);
147
148 let response = self
150 .client
151 .send(request)
152 .await
153 .map_err(|e| LastFmError::Http(e.to_string()))?;
154
155 log::debug!("📥 Login response status: {}", response.status());
156 log::debug!(
157 "📥 Login response headers: {:?}",
158 response.iter().collect::<Vec<_>>()
159 );
160
161 Ok(response)
162 }
163
164 fn create_login_request(&self, login_url: &str, cookies: &[String]) -> Result<Request> {
166 let mut request = Request::new(Method::Post, login_url.parse::<Url>().unwrap());
167
168 let _ = request.insert_header("Referer", login_url);
170 let _ = request.insert_header("Origin", &self.base_url);
171 let _ = request.insert_header("Content-Type", "application/x-www-form-urlencoded");
172 let _ = request.insert_header(
173 "User-Agent",
174 "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
175 );
176 let _ = request.insert_header(
177 "Accept",
178 "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
179 );
180 let _ = request.insert_header("Accept-Language", "en-US,en;q=0.9");
181 let _ = request.insert_header("Accept-Encoding", "gzip, deflate, br");
182 let _ = request.insert_header("DNT", "1");
183 let _ = request.insert_header("Connection", "keep-alive");
184 let _ = request.insert_header("Upgrade-Insecure-Requests", "1");
185 let _ = request.insert_header(
186 "sec-ch-ua",
187 "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Google Chrome\";v=\"138\"",
188 );
189 let _ = request.insert_header("sec-ch-ua-mobile", "?0");
190 let _ = request.insert_header("sec-ch-ua-platform", "\"Linux\"");
191 let _ = request.insert_header("Sec-Fetch-Dest", "document");
192 let _ = request.insert_header("Sec-Fetch-Mode", "navigate");
193 let _ = request.insert_header("Sec-Fetch-Site", "same-origin");
194 let _ = request.insert_header("Sec-Fetch-User", "?1");
195
196 if !cookies.is_empty() {
198 let cookie_header = cookies.join("; ");
199 let _ = request.insert_header("Cookie", &cookie_header);
200 }
201
202 Ok(request)
203 }
204
205 async fn validate_login_response(
207 &self,
208 mut response: http_types::Response,
209 username: &str,
210 cookies: Vec<String>,
211 csrf_token: String,
212 ) -> Result<LastFmEditSession> {
213 if response.status() == 403 {
215 return self.handle_403_response(response).await;
216 }
217
218 if let Some(session) =
220 self.check_session_success(&response, username, &cookies, &csrf_token)
221 {
222 return Ok(session);
223 }
224
225 let response_html = response
227 .body_string()
228 .await
229 .map_err(|e| LastFmError::Http(e.to_string()))?;
230
231 log::debug!(
232 "📄 Login response HTML length: {} chars",
233 response_html.len()
234 );
235 if response_html.len() < 500 {
236 log::debug!("📄 Login response HTML content (short): {response_html}");
237 }
238
239 let has_login_form = self.check_for_login_form(&response_html);
241 log::debug!("🔍 Final login validation:");
242 log::debug!(" - Response contains login form: {has_login_form}");
243 log::debug!(" - Response status: {}", response.status());
244
245 if !has_login_form && response.status() == 200 {
246 log::info!("✅ Login successful - no login form detected in response");
247 Ok(LastFmEditSession::new(
248 username.to_string(),
249 cookies,
250 Some(csrf_token),
251 self.base_url.clone(),
252 ))
253 } else {
254 let error_msg = self.parse_login_error(&response_html);
256 log::warn!("❌ Login failed: {error_msg}");
257 Err(LastFmError::Auth(error_msg))
258 }
259 }
260
261 async fn handle_403_response(
263 &self,
264 mut response: http_types::Response,
265 ) -> Result<LastFmEditSession> {
266 let response_html = response
267 .body_string()
268 .await
269 .map_err(|e| LastFmError::Http(e.to_string()))?;
270
271 log::debug!("📄 403 response HTML length: {} chars", response_html.len());
272 if response_html.len() < 2000 {
273 log::debug!("📄 403 response HTML content: {response_html}");
274 } else {
275 log::debug!("📄 403 response HTML start: {}", &response_html[..500]);
277 log::debug!(
278 "📄 403 response HTML end: {}",
279 &response_html[response_html.len() - 500..]
280 );
281 }
282
283 let login_error = self.parse_login_error(&response_html);
284 Err(LastFmError::Auth(login_error))
285 }
286
287 fn check_session_success(
289 &self,
290 response: &http_types::Response,
291 username: &str,
292 cookies: &[String],
293 csrf_token: &str,
294 ) -> Option<LastFmEditSession> {
295 let has_real_session = cookies
296 .iter()
297 .any(|cookie| cookie.starts_with("sessionid=.") && cookie.len() > 50);
298
299 log::debug!("🔍 Session validation:");
300 log::debug!(" - Has real session cookie: {has_real_session}");
301 log::debug!(" - Response status: {}", response.status());
302 log::debug!(" - All cookies: {cookies:?}");
303
304 if has_real_session && (response.status() == 302 || response.status() == 200) {
305 log::info!("✅ Login successful - authenticated session established");
306 Some(LastFmEditSession::new(
307 username.to_string(),
308 cookies.to_vec(),
309 Some(csrf_token.to_string()),
310 self.base_url.clone(),
311 ))
312 } else {
313 None
314 }
315 }
316
317 async fn get(&self, url: &str) -> Result<http_types::Response> {
319 let mut request = Request::new(Method::Get, url.parse::<Url>().unwrap());
320 let _ = request.insert_header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36");
321
322 self.client
323 .send(request)
324 .await
325 .map_err(|e| LastFmError::Http(e.to_string()))
326 }
327
328 fn extract_login_form_data(&self, html: &str) -> Result<(String, Option<String>)> {
330 let document = Html::parse_document(html);
331
332 let csrf_token = self.extract_csrf_token(&document)?;
333
334 let next_selector = Selector::parse("input[name=\"next\"]").unwrap();
336 let next_field = document
337 .select(&next_selector)
338 .next()
339 .and_then(|input| input.value().attr("value"))
340 .map(|s| s.to_string());
341
342 Ok((csrf_token, next_field))
343 }
344
345 fn extract_csrf_token(&self, document: &Html) -> Result<String> {
346 let csrf_selector = Selector::parse("input[name=\"csrfmiddlewaretoken\"]").unwrap();
347
348 let csrf_token = document
349 .select(&csrf_selector)
350 .next()
351 .and_then(|input| input.value().attr("value"))
352 .map(|token| token.to_string())
353 .ok_or(LastFmError::CsrfNotFound)?;
354
355 log::debug!("🔑 CSRF token extracted from HTML: {csrf_token}");
356 Ok(csrf_token)
357 }
358
359 fn parse_login_error(&self, html: &str) -> String {
361 let document = Html::parse_document(html);
362
363 let error_selector = Selector::parse(".alert-danger, .form-error, .error-message").unwrap();
364
365 let mut error_messages = Vec::new();
366 for error in document.select(&error_selector) {
367 let error_text = error.text().collect::<String>().trim().to_string();
368 if !error_text.is_empty() {
369 error_messages.push(error_text);
370 }
371 }
372
373 if error_messages.is_empty() {
374 "Login failed - please check your credentials".to_string()
375 } else {
376 format!("Login failed: {}", error_messages.join("; "))
377 }
378 }
379
380 fn check_for_login_form(&self, html: &str) -> bool {
382 let document = Html::parse_document(html);
383 let login_form_selector =
384 Selector::parse("form[action*=\"login\"], input[name=\"username_or_email\"]").unwrap();
385 document.select(&login_form_selector).next().is_some()
386 }
387}
388
389pub fn extract_cookies_from_response(response: &http_types::Response, cookies: &mut Vec<String>) {
391 if let Some(cookie_headers) = response.header("set-cookie") {
392 for cookie_header in cookie_headers {
393 let cookie_str = cookie_header.as_str();
394 if let Some(cookie_value) = cookie_str.split(';').next() {
396 let cookie_name = cookie_value.split('=').next().unwrap_or("");
397
398 cookies.retain(|existing| !existing.starts_with(&format!("{cookie_name}=")));
400 cookies.push(cookie_value.to_string());
401 }
402 }
403 }
404}