1use crate::session::LastFmEditSession;
2use crate::{LastFmError, 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 let login_url = format!("{}/login", self.base_url);
39 let mut response = self.get(&login_url).await?;
40
41 let mut cookies = Vec::new();
43 extract_cookies_from_response(&response, &mut cookies);
44
45 let html = response
46 .body_string()
47 .await
48 .map_err(|e| LastFmError::Http(e.to_string()))?;
49
50 let (csrf_token, next_field) = self.extract_login_form_data(&html)?;
52
53 let mut form_data = HashMap::new();
55 form_data.insert("csrfmiddlewaretoken", csrf_token.as_str());
56 form_data.insert("username_or_email", username);
57 form_data.insert("password", password);
58
59 if let Some(ref next_value) = next_field {
61 form_data.insert("next", next_value);
62 }
63
64 let mut request = Request::new(Method::Post, login_url.parse::<Url>().unwrap());
65 let _ = request.insert_header("Referer", &login_url);
66 let _ = request.insert_header("Origin", &self.base_url);
67 let _ = request.insert_header("Content-Type", "application/x-www-form-urlencoded");
68 let _ = request.insert_header(
69 "User-Agent",
70 "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
71 );
72 let _ = request.insert_header(
73 "Accept",
74 "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"
75 );
76 let _ = request.insert_header("Accept-Language", "en-US,en;q=0.9");
77 let _ = request.insert_header("Accept-Encoding", "gzip, deflate, br");
78 let _ = request.insert_header("DNT", "1");
79 let _ = request.insert_header("Connection", "keep-alive");
80 let _ = request.insert_header("Upgrade-Insecure-Requests", "1");
81 let _ = request.insert_header(
82 "sec-ch-ua",
83 "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Google Chrome\";v=\"138\"",
84 );
85 let _ = request.insert_header("sec-ch-ua-mobile", "?0");
86 let _ = request.insert_header("sec-ch-ua-platform", "\"Linux\"");
87 let _ = request.insert_header("Sec-Fetch-Dest", "document");
88 let _ = request.insert_header("Sec-Fetch-Mode", "navigate");
89 let _ = request.insert_header("Sec-Fetch-Site", "same-origin");
90 let _ = request.insert_header("Sec-Fetch-User", "?1");
91
92 if !cookies.is_empty() {
94 let cookie_header = cookies.join("; ");
95 let _ = request.insert_header("Cookie", &cookie_header);
96 }
97
98 let form_string: String = form_data
100 .iter()
101 .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
102 .collect::<Vec<_>>()
103 .join("&");
104
105 request.set_body(form_string);
106
107 let mut response = self
108 .client
109 .send(request)
110 .await
111 .map_err(|e| LastFmError::Http(e.to_string()))?;
112
113 extract_cookies_from_response(&response, &mut cookies);
115
116 log::debug!("Login response status: {}", response.status());
117
118 if response.status() == 403 {
120 let response_html = response
121 .body_string()
122 .await
123 .map_err(|e| LastFmError::Http(e.to_string()))?;
124
125 let login_error = self.parse_login_error(&response_html);
126 return Err(LastFmError::Auth(login_error));
127 }
128
129 let has_real_session = cookies
131 .iter()
132 .any(|cookie| cookie.starts_with("sessionid=.") && cookie.len() > 50);
133
134 if has_real_session && (response.status() == 302 || response.status() == 200) {
135 log::debug!("Login successful - authenticated session established");
137 return Ok(LastFmEditSession::new(
138 username.to_string(),
139 cookies,
140 Some(csrf_token),
141 self.base_url.clone(),
142 ));
143 }
144
145 let response_html = response
147 .body_string()
148 .await
149 .map_err(|e| LastFmError::Http(e.to_string()))?;
150
151 let has_login_form = self.check_for_login_form(&response_html);
153
154 if !has_login_form && response.status() == 200 {
155 Ok(LastFmEditSession::new(
156 username.to_string(),
157 cookies,
158 Some(csrf_token),
159 self.base_url.clone(),
160 ))
161 } else {
162 let error_msg = self.parse_login_error(&response_html);
164 Err(LastFmError::Auth(error_msg))
165 }
166 }
167
168 async fn get(&self, url: &str) -> Result<http_types::Response> {
170 let mut request = Request::new(Method::Get, url.parse::<Url>().unwrap());
171 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");
172
173 self.client
174 .send(request)
175 .await
176 .map_err(|e| LastFmError::Http(e.to_string()))
177 }
178
179 fn extract_login_form_data(&self, html: &str) -> Result<(String, Option<String>)> {
181 let document = Html::parse_document(html);
182
183 let csrf_token = self.extract_csrf_token(&document)?;
184
185 let next_selector = Selector::parse("input[name=\"next\"]").unwrap();
187 let next_field = document
188 .select(&next_selector)
189 .next()
190 .and_then(|input| input.value().attr("value"))
191 .map(|s| s.to_string());
192
193 Ok((csrf_token, next_field))
194 }
195
196 fn extract_csrf_token(&self, document: &Html) -> Result<String> {
197 let csrf_selector = Selector::parse("input[name=\"csrfmiddlewaretoken\"]").unwrap();
198
199 document
200 .select(&csrf_selector)
201 .next()
202 .and_then(|input| input.value().attr("value"))
203 .map(|token| token.to_string())
204 .ok_or(LastFmError::CsrfNotFound)
205 }
206
207 fn parse_login_error(&self, html: &str) -> String {
209 let document = Html::parse_document(html);
210
211 let error_selector = Selector::parse(".alert-danger, .form-error, .error-message").unwrap();
212
213 let mut error_messages = Vec::new();
214 for error in document.select(&error_selector) {
215 let error_text = error.text().collect::<String>().trim().to_string();
216 if !error_text.is_empty() {
217 error_messages.push(error_text);
218 }
219 }
220
221 if error_messages.is_empty() {
222 "Login failed - please check your credentials".to_string()
223 } else {
224 format!("Login failed: {}", error_messages.join("; "))
225 }
226 }
227
228 fn check_for_login_form(&self, html: &str) -> bool {
230 let document = Html::parse_document(html);
231 let login_form_selector =
232 Selector::parse("form[action*=\"login\"], input[name=\"username_or_email\"]").unwrap();
233 document.select(&login_form_selector).next().is_some()
234 }
235}
236
237pub fn extract_cookies_from_response(response: &http_types::Response, cookies: &mut Vec<String>) {
239 if let Some(cookie_headers) = response.header("set-cookie") {
240 for cookie_header in cookie_headers {
241 let cookie_str = cookie_header.as_str();
242 if let Some(cookie_value) = cookie_str.split(';').next() {
244 let cookie_name = cookie_value.split('=').next().unwrap_or("");
245
246 cookies.retain(|existing| !existing.starts_with(&format!("{cookie_name}=")));
248 cookies.push(cookie_value.to_string());
249 }
250 }
251 }
252}