1use crate::persistence::{PendingEdit, PendingRewriteRule, RewriteRulesState};
2use crate::rewrite::{RewriteError, RewriteRule};
3use async_trait::async_trait;
4use lastfm_edit::{ScrobbleEdit, Track};
5use std::error::Error;
6use std::fmt;
7
8#[derive(Debug)]
10pub struct ActionProviderError(pub String);
11
12impl fmt::Display for ActionProviderError {
13 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14 write!(f, "Action provider error: {}", self.0)
15 }
16}
17
18impl Error for ActionProviderError {}
19
20impl From<RewriteError> for ActionProviderError {
21 fn from(err: RewriteError) -> Self {
22 Self(format!("Rewrite error: {err}"))
23 }
24}
25
26impl From<String> for ActionProviderError {
27 fn from(msg: String) -> Self {
28 Self(msg)
29 }
30}
31
32impl From<&str> for ActionProviderError {
33 fn from(msg: &str) -> Self {
34 Self(msg.to_string())
35 }
36}
37
38#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
40pub enum ScrubActionSuggestion {
41 Edit(ScrobbleEdit),
43 ProposeRule {
45 rule: RewriteRule,
46 motivation: String,
47 },
48 NoAction,
50}
51
52#[derive(Debug, Clone)]
54pub struct SuggestionWithContext {
55 pub suggestion: ScrubActionSuggestion,
56 pub requires_confirmation: bool,
57 pub provider_name: String,
58}
59
60impl SuggestionWithContext {
61 pub fn new(
62 suggestion: ScrubActionSuggestion,
63 requires_confirmation: bool,
64 provider_name: String,
65 ) -> Self {
66 Self {
67 suggestion,
68 requires_confirmation,
69 provider_name,
70 }
71 }
72
73 pub fn edit_with_confirmation(
74 edit: ScrobbleEdit,
75 requires_confirmation: bool,
76 provider_name: String,
77 ) -> Self {
78 Self::new(
79 ScrubActionSuggestion::Edit(edit),
80 requires_confirmation,
81 provider_name,
82 )
83 }
84
85 pub fn propose_rule_with_confirmation(
86 rule: RewriteRule,
87 motivation: String,
88 requires_confirmation: bool,
89 provider_name: String,
90 ) -> Self {
91 Self::new(
92 ScrubActionSuggestion::ProposeRule { rule, motivation },
93 requires_confirmation,
94 provider_name,
95 )
96 }
97
98 pub fn no_action(provider_name: String) -> Self {
99 Self::new(ScrubActionSuggestion::NoAction, false, provider_name)
100 }
101}
102
103#[async_trait]
105pub trait ScrubActionProvider: Send + Sync {
106 type Error: std::error::Error + Send + Sync + 'static;
107
108 async fn analyze_tracks(
115 &self,
116 tracks: &[Track],
117 pending_edits: Option<&[PendingEdit]>,
118 pending_rules: Option<&[PendingRewriteRule]>,
119 ) -> Result<Vec<(usize, Vec<SuggestionWithContext>)>, Self::Error>;
120
121 fn provider_name(&self) -> &str;
123}
124
125pub struct RewriteRulesScrubActionProvider {
127 rules: Vec<RewriteRule>,
128}
129
130impl RewriteRulesScrubActionProvider {
131 #[must_use]
132 pub fn new(rules_state: &RewriteRulesState) -> Self {
133 Self {
134 rules: rules_state.rewrite_rules.clone(),
135 }
136 }
137
138 #[must_use]
139 pub const fn from_rules(rules: Vec<RewriteRule>) -> Self {
140 Self { rules }
141 }
142
143 async fn apply_rules_sequentially(
146 &self,
147 track: &Track,
148 ) -> Result<Option<(ScrobbleEdit, bool)>, ActionProviderError> {
149 let mut edit = crate::rewrite::create_no_op_edit(track);
150 let mut any_changes = false;
151 let mut requires_confirmation_applied = false;
152
153 for rule in &self.rules {
154 if !rule.matches_scrobble_edit(&edit)? {
155 continue;
156 }
157
158 let mut candidate = edit.clone();
159 let changed = rule.apply(&mut candidate)?;
160 if !changed {
161 continue;
162 }
163
164 if rule.requires_musicbrainz_confirmation {
165 let confirmed = Self::verify_with_musicbrainz_using_rule_filters(
166 &candidate,
167 track,
168 rule.musicbrainz_release_filters.as_ref(),
169 )
170 .await?;
171 if !confirmed {
172 log::info!(
173 "Rewrite rule '{}' rejected by MusicBrainz confirmation for track '{} - {}' (album: {})",
174 rule.name.as_deref().unwrap_or("Unnamed"),
175 track.artist,
176 track.name,
177 track.album.as_deref().unwrap_or("none")
178 );
179 continue; }
181 }
182
183 edit = candidate;
185 any_changes = true;
186 requires_confirmation_applied |= rule.requires_confirmation;
187 }
188
189 if any_changes {
190 Ok(Some((edit, requires_confirmation_applied)))
191 } else {
192 Ok(None)
193 }
194 }
195
196 async fn verify_with_musicbrainz_using_rule_filters(
198 candidate: &ScrobbleEdit,
199 track: &Track,
200 release_filters: Option<&crate::config::ReleaseFilterConfig>,
201 ) -> Result<bool, ActionProviderError> {
202 let artist = candidate.artist_name.clone();
203 let title = candidate
204 .track_name
205 .clone()
206 .unwrap_or_else(|| track.name.clone());
207 let album = candidate.album_name.clone();
208
209 if let Some(filters) = release_filters {
211 crate::musicbrainz_provider::MusicBrainzScrubActionProvider::verify_track_exists_with_filters(
213 &artist,
214 &title,
215 album.as_deref(),
216 filters,
217 )
218 .await
219 .map_err(|e| ActionProviderError(format!("MusicBrainz verification failed: {e}")))
220 } else {
221 let default_provider =
223 crate::musicbrainz_provider::MusicBrainzScrubActionProvider::new(0.8, 20);
224 default_provider
225 .verify_track_exists(&artist, &title, album.as_deref())
226 .await
227 .map_err(|e| ActionProviderError(format!("MusicBrainz verification failed: {e}")))
228 }
229 }
230}
231
232#[async_trait]
233impl ScrubActionProvider for RewriteRulesScrubActionProvider {
234 type Error = ActionProviderError;
235
236 async fn analyze_tracks(
237 &self,
238 tracks: &[Track],
239 _pending_edits: Option<&[crate::persistence::PendingEdit]>,
240 _pending_rules: Option<&[crate::persistence::PendingRewriteRule]>,
241 ) -> Result<Vec<(usize, Vec<SuggestionWithContext>)>, Self::Error> {
242 let mut results = Vec::new();
243
244 for (index, track) in tracks.iter().enumerate() {
245 log::trace!("RewriteRulesScrubActionProvider analyzing track {index}: '{track_name}' by '{track_artist}' against {rules_count} rules",
246 track_name = track.name, track_artist = track.artist, rules_count = self.rules.len());
247
248 if !crate::rewrite::any_rules_apply(&self.rules, track)? {
250 log::trace!(
251 "RewriteRulesScrubActionProvider track {index}: no rules apply, skipping"
252 );
253 continue;
254 }
255
256 if let Some((final_edit, requires_confirmation)) =
258 self.apply_rules_sequentially(track).await?
259 {
260 results.push((
261 index,
262 vec![SuggestionWithContext::edit_with_confirmation(
263 final_edit,
264 requires_confirmation,
265 self.provider_name().to_string(),
266 )],
267 ));
268 }
269 }
270
271 Ok(results)
272 }
273
274 fn provider_name(&self) -> &'static str {
275 "RewriteRules"
276 }
277}
278
279pub struct OrScrubActionProvider {
281 providers: Vec<Box<dyn ScrubActionProvider<Error = ActionProviderError>>>,
282 provider_names: Vec<String>,
283}
284
285impl Default for OrScrubActionProvider {
286 fn default() -> Self {
287 Self::new()
288 }
289}
290
291impl OrScrubActionProvider {
292 #[must_use]
293 pub fn new() -> Self {
294 Self {
295 providers: Vec::new(),
296 provider_names: Vec::new(),
297 }
298 }
299
300 pub fn add_provider<P>(mut self, provider: P) -> Self
301 where
302 P: ScrubActionProvider + 'static,
303 P::Error: Into<ActionProviderError>,
304 {
305 let name = provider.provider_name().to_string();
306 self.provider_names.push(name);
307
308 let wrapped_provider = ErrorAdapter { inner: provider };
310 self.providers.push(Box::new(wrapped_provider));
311 self
312 }
313
314 #[must_use]
315 pub fn with_providers<P>(providers: Vec<P>) -> Self
316 where
317 P: ScrubActionProvider + 'static,
318 P::Error: Into<ActionProviderError>,
319 {
320 let mut or_provider = Self::new();
321 for provider in providers {
322 or_provider = or_provider.add_provider(provider);
323 }
324 or_provider
325 }
326}
327
328struct ErrorAdapter<P> {
330 inner: P,
331}
332
333#[async_trait]
334impl<P> ScrubActionProvider for ErrorAdapter<P>
335where
336 P: ScrubActionProvider + Send + Sync,
337 P::Error: Into<ActionProviderError>,
338{
339 type Error = ActionProviderError;
340
341 async fn analyze_tracks(
342 &self,
343 tracks: &[Track],
344 pending_edits: Option<&[PendingEdit]>,
345 pending_rules: Option<&[PendingRewriteRule]>,
346 ) -> Result<Vec<(usize, Vec<SuggestionWithContext>)>, Self::Error> {
347 self.inner
348 .analyze_tracks(tracks, pending_edits, pending_rules)
349 .await
350 .map_err(std::convert::Into::into)
351 }
352
353 fn provider_name(&self) -> &str {
354 self.inner.provider_name()
355 }
356}
357
358#[async_trait]
359impl ScrubActionProvider for OrScrubActionProvider {
360 type Error = ActionProviderError;
361
362 async fn analyze_tracks(
363 &self,
364 tracks: &[Track],
365 pending_edits: Option<&[PendingEdit]>,
366 pending_rules: Option<&[PendingRewriteRule]>,
367 ) -> Result<Vec<(usize, Vec<SuggestionWithContext>)>, Self::Error> {
368 let mut combined_results: Vec<(usize, Vec<SuggestionWithContext>)> = Vec::new();
369
370 for (provider_idx, provider) in self.providers.iter().enumerate() {
372 match provider
373 .analyze_tracks(tracks, pending_edits, pending_rules)
374 .await
375 {
376 Ok(provider_results) => {
377 for (track_idx, suggestions) in provider_results {
379 if let Some(existing) = combined_results
381 .iter_mut()
382 .find(|(idx, _)| *idx == track_idx)
383 {
384 existing.1.extend(suggestions);
386 } else {
387 combined_results.push((track_idx, suggestions));
389 }
390 }
391 }
392 Err(e) => {
393 log::warn!(
395 "Error from provider '{}': {}",
396 self.provider_names
397 .get(provider_idx)
398 .unwrap_or(&"unknown".to_string()),
399 e
400 );
401 }
402 }
403 }
404
405 Ok(combined_results)
406 }
407
408 fn provider_name(&self) -> &'static str {
409 "OrProvider"
410 }
411}