lastfm_edit/
edit_analysis.rs1use http_types::StatusCode;
2use scraper::{Html, Selector};
3
4#[derive(Debug, Clone)]
6pub struct EditAnalysisResult {
7 pub success: bool,
9 pub message: Option<String>,
11 pub actual_track_name: Option<String>,
13 pub actual_album_name: Option<String>,
15}
16
17pub fn analyze_edit_response(response_text: &str, status_code: StatusCode) -> EditAnalysisResult {
29 let document = Html::parse_document(response_text);
31
32 let success_selector = Selector::parse(".alert-success").unwrap();
34 let error_selector = Selector::parse(".alert-danger, .alert-error, .error").unwrap();
35
36 let has_success_alert = document.select(&success_selector).next().is_some();
37 let has_error_alert = document.select(&error_selector).next().is_some();
38
39 let (actual_track_name, actual_album_name) =
41 extract_track_album_names(&document, response_text);
42
43 log::debug!(
44 "Response analysis: success_alert={}, error_alert={}, track='{}', album='{}'",
45 has_success_alert,
46 has_error_alert,
47 actual_track_name.as_deref().unwrap_or("not found"),
48 actual_album_name.as_deref().unwrap_or("not found")
49 );
50
51 let final_success = status_code.is_success() && has_success_alert && !has_error_alert;
53
54 let message = if has_error_alert {
56 if let Some(error_element) = document.select(&error_selector).next() {
58 Some(format!(
59 "Edit failed: {}",
60 error_element.text().collect::<String>().trim()
61 ))
62 } else {
63 Some("Edit failed with unknown error".to_string())
64 }
65 } else if final_success {
66 Some(format!(
67 "Edit successful - Track: '{}', Album: '{}'",
68 actual_track_name.as_deref().unwrap_or("unknown"),
69 actual_album_name.as_deref().unwrap_or("unknown")
70 ))
71 } else {
72 Some(format!("Edit failed with status: {status_code}"))
73 };
74
75 EditAnalysisResult {
76 success: final_success,
77 message,
78 actual_track_name,
79 actual_album_name,
80 }
81}
82
83fn extract_track_album_names(
88 document: &Html,
89 response_text: &str,
90) -> (Option<String>, Option<String>) {
91 let mut actual_track_name = None;
92 let mut actual_album_name = None;
93
94 let track_name_selector = Selector::parse("td.chartlist-name a").unwrap();
96 let album_name_selector = Selector::parse("td.chartlist-album a").unwrap();
97
98 if let Some(track_element) = document.select(&track_name_selector).next() {
99 actual_track_name = Some(track_element.text().collect::<String>().trim().to_string());
100 }
101
102 if let Some(album_element) = document.select(&album_name_selector).next() {
103 actual_album_name = Some(album_element.text().collect::<String>().trim().to_string());
104 }
105
106 if actual_track_name.is_none() || actual_album_name.is_none() {
108 if actual_track_name.is_none() {
109 actual_track_name = extract_track_name_from_text(response_text);
110 }
111
112 if actual_album_name.is_none() {
113 actual_album_name = extract_album_name_from_text(response_text);
114 }
115 }
116
117 (actual_track_name, actual_album_name)
118}
119
120fn extract_track_name_from_text(response_text: &str) -> Option<String> {
122 let track_pattern = regex::Regex::new(r#"href="/music/[^"]+/_/([^"]+)""#).unwrap();
125 if let Some(captures) = track_pattern.captures(response_text) {
126 if let Some(track_match) = captures.get(1) {
127 let raw_track = track_match.as_str();
128 let decoded_track = urlencoding::decode(raw_track)
130 .unwrap_or_else(|_| raw_track.into())
131 .replace('+', " ");
132 return Some(decoded_track);
133 }
134 }
135 None
136}
137
138fn extract_album_name_from_text(response_text: &str) -> Option<String> {
140 let album_pattern =
143 regex::Regex::new(r#"href="/music/[^"]+/([^"/_]+)"[^>]*>[^<]*</a>"#).unwrap();
144 if let Some(captures) = album_pattern.captures(response_text) {
145 if let Some(album_match) = captures.get(1) {
146 let raw_album = album_match.as_str();
147 let decoded_album = urlencoding::decode(raw_album)
149 .unwrap_or_else(|_| raw_album.into())
150 .replace('+', " ");
151 return Some(decoded_album);
152 }
153 }
154 None
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn test_analyze_success_response() {
163 let html = r#"
164 <div class="alert-success">Edit successful</div>
165 <table>
166 <tr>
167 <td class="chartlist-name"><a href="/music/artist/_/track">Test Track</a></td>
168 <td class="chartlist-album"><a href="/music/artist/album">Test Album</a></td>
169 </tr>
170 </table>
171 "#;
172
173 let result = analyze_edit_response(html, StatusCode::Ok);
174 assert!(result.success);
175 assert_eq!(result.actual_track_name, Some("Test Track".to_string()));
177 assert_eq!(result.actual_album_name, Some("Test Album".to_string()));
178 }
179
180 #[test]
181 fn test_analyze_error_response() {
182 let html = r#"
183 <div class="alert-danger">Edit failed: Invalid data</div>
184 "#;
185
186 let result = analyze_edit_response(html, StatusCode::Ok);
187 assert!(!result.success);
188 assert!(result
189 .message
190 .unwrap()
191 .contains("Edit failed: Invalid data"));
192 }
193
194 #[test]
195 fn test_extract_from_regex_patterns() {
196 let html = r#"
197 Some content with <a href="/music/Artist/AlbumName">album link</a>
198 and later <a href="/music/Artist/_/TrackName">track link</a>
199 "#;
200
201 let result = analyze_edit_response(html, StatusCode::Ok);
202 assert_eq!(result.actual_track_name, Some("TrackName".to_string()));
205 assert_eq!(result.actual_album_name, Some("AlbumName".to_string()));
206 }
207}