Skip to main content

scrobble_scrubber/persistence/
mod.rs

1use async_trait::async_trait;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4// use uuid::Uuid;
5
6use crate::rewrite::RewriteRule;
7
8/// Preview of rule transformation showing changes
9#[derive(Debug, Clone)]
10pub struct RuleTransformationPreview {
11    pub original_track_name: String,
12    pub original_artist_name: String,
13    pub original_album_name: Option<String>,
14    pub original_album_artist_name: Option<String>,
15    pub transformed_track_name: Option<String>,
16    pub transformed_artist_name: Option<String>,
17    pub transformed_album_name: Option<String>,
18    pub transformed_album_artist_name: Option<String>,
19}
20
21/// Core persistence traits and types
22#[derive(Debug, Clone, Serialize, Deserialize, Default)]
23pub struct TimestampState {
24    pub last_processed_timestamp: Option<DateTime<Utc>>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize, Default)]
28pub struct RewriteRulesState {
29    pub rewrite_rules: Vec<RewriteRule>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct PendingEdit {
34    pub id: String,
35    pub original_track_name: String,
36    pub original_artist_name: String,
37    pub original_album_name: Option<String>,
38    pub original_album_artist_name: Option<String>,
39    pub new_track_name: Option<String>,
40    pub new_artist_name: Option<String>,
41    pub new_album_name: Option<String>,
42    pub new_album_artist_name: Option<String>,
43    pub timestamp: Option<u64>,
44}
45
46impl PendingEdit {
47    #[allow(clippy::too_many_arguments)]
48    pub fn new(
49        original_track_name: String,
50        original_artist_name: String,
51        original_album_name: Option<String>,
52        original_album_artist_name: Option<String>,
53        new_track_name: Option<String>,
54        new_artist_name: Option<String>,
55        new_album_name: Option<String>,
56        new_album_artist_name: Option<String>,
57        timestamp: Option<u64>,
58    ) -> Self {
59        Self {
60            id: format!(
61                "id-{}",
62                chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)
63            ),
64            original_track_name,
65            original_artist_name,
66            original_album_name,
67            original_album_artist_name,
68            new_track_name,
69            new_artist_name,
70            new_album_name,
71            new_album_artist_name,
72            timestamp,
73        }
74    }
75
76    /// Convert this PendingEdit to a ScrobbleEdit for applying to Last.fm
77    pub fn to_scrobble_edit(&self) -> lastfm_edit::ScrobbleEdit {
78        lastfm_edit::ScrobbleEdit {
79            track_name_original: Some(self.original_track_name.clone()),
80            album_name_original: self.original_album_name.clone(),
81            artist_name_original: self.original_artist_name.clone(),
82            album_artist_name_original: self.original_album_artist_name.clone(),
83            track_name: self
84                .new_track_name
85                .clone()
86                .or_else(|| Some(self.original_track_name.clone())),
87            album_name: self
88                .new_album_name
89                .clone()
90                .or(self.original_album_name.clone()),
91            artist_name: self
92                .new_artist_name
93                .clone()
94                .unwrap_or_else(|| self.original_artist_name.clone()),
95            album_artist_name: self
96                .new_album_artist_name
97                .clone()
98                .or(self.original_album_artist_name.clone()),
99            timestamp: self.timestamp,
100            edit_all: true,
101        }
102    }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct PendingRewriteRule {
107    pub id: String,
108    pub rule: RewriteRule,
109    pub reason: String,
110    pub example_track_name: String,
111    pub example_artist_name: String,
112    pub example_album_name: Option<String>,
113    pub example_album_artist_name: Option<String>,
114}
115
116impl PendingRewriteRule {
117    pub fn new(
118        rule: RewriteRule,
119        reason: String,
120        example_track_name: String,
121        example_artist_name: String,
122    ) -> Self {
123        Self {
124            id: format!(
125                "id-{}",
126                chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)
127            ),
128            rule,
129            reason,
130            example_track_name,
131            example_artist_name,
132            example_album_name: None,
133            example_album_artist_name: None,
134        }
135    }
136
137    pub fn new_with_album_info(
138        rule: RewriteRule,
139        reason: String,
140        example_track_name: String,
141        example_artist_name: String,
142        example_album_name: Option<String>,
143        example_album_artist_name: Option<String>,
144    ) -> Self {
145        Self {
146            id: format!(
147                "id-{}",
148                chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)
149            ),
150            rule,
151            reason,
152            example_track_name,
153            example_artist_name,
154            example_album_name,
155            example_album_artist_name,
156        }
157    }
158
159    /// Apply the rule to the example and return a preview
160    pub fn apply_rule_to_example(
161        &self,
162    ) -> Result<RuleTransformationPreview, crate::rewrite::RewriteError> {
163        // Create a mock track from the example
164        let track = lastfm_edit::Track {
165            name: self.example_track_name.clone(),
166            artist: self.example_artist_name.clone(),
167            playcount: 1,                // placeholder
168            timestamp: Some(1640995200), // placeholder timestamp
169            album: self.example_album_name.clone(),
170            album_artist: None, // placeholder
171        };
172
173        let original_track_name = self.example_track_name.clone();
174        let original_artist_name = self.example_artist_name.clone();
175        let original_album_name = self.example_album_name.clone();
176        let original_album_artist_name = self.example_album_artist_name.clone();
177
178        // Apply the rule to see the transformation
179        if self.rule.matches(&track)? {
180            let mut edit = crate::rewrite::create_no_op_edit(&track);
181            let changed = self.rule.apply(&mut edit)?;
182
183            if changed {
184                Ok(RuleTransformationPreview {
185                    original_track_name: original_track_name.clone(),
186                    original_artist_name: original_artist_name.clone(),
187                    original_album_name: original_album_name.clone(),
188                    original_album_artist_name: original_album_artist_name.clone(),
189                    transformed_track_name: if edit.track_name.as_ref()
190                        != Some(&original_track_name)
191                    {
192                        edit.track_name
193                    } else {
194                        None
195                    },
196                    transformed_artist_name: if Some(&edit.artist_name)
197                        != Some(&original_artist_name)
198                    {
199                        Some(edit.artist_name)
200                    } else {
201                        None
202                    },
203                    transformed_album_name: if original_album_name
204                        .as_ref()
205                        .is_some_and(|orig| edit.album_name.as_ref() != Some(orig))
206                    {
207                        edit.album_name
208                    } else {
209                        None
210                    },
211                    transformed_album_artist_name: if original_album_artist_name
212                        .as_ref()
213                        .is_some_and(|orig| edit.album_artist_name.as_ref() != Some(orig))
214                    {
215                        edit.album_artist_name
216                    } else {
217                        None
218                    },
219                })
220            } else {
221                Ok(RuleTransformationPreview {
222                    original_track_name,
223                    original_artist_name,
224                    original_album_name,
225                    original_album_artist_name,
226                    transformed_track_name: None,
227                    transformed_artist_name: None,
228                    transformed_album_name: None,
229                    transformed_album_artist_name: None,
230                })
231            }
232        } else {
233            Ok(RuleTransformationPreview {
234                original_track_name,
235                original_artist_name,
236                original_album_name,
237                original_album_artist_name,
238                transformed_track_name: None,
239                transformed_artist_name: None,
240                transformed_album_name: None,
241                transformed_album_artist_name: None,
242            })
243        }
244    }
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize, Default)]
248pub struct PendingEditsState {
249    pub pending_edits: Vec<PendingEdit>,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize, Default)]
253pub struct PendingRewriteRulesState {
254    pub pending_rules: Vec<PendingRewriteRule>,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize, Default)]
258pub struct SettingsState {
259    /// Global setting to require confirmation for all edits
260    pub require_confirmation: bool,
261    /// Global setting to require confirmation for edit operations specifically
262    pub require_confirmation_for_edits: bool,
263    /// Global setting to require confirmation for new rule creation
264    pub require_confirmation_for_new_rules: bool,
265}
266
267/// Main persistence trait
268#[async_trait]
269pub trait StateStorage: Send + Sync {
270    type Error: std::error::Error + Send + Sync + 'static;
271
272    async fn save_timestamp_state(&mut self, state: &TimestampState) -> Result<(), Self::Error>;
273    async fn load_timestamp_state(&self) -> Result<TimestampState, Self::Error>;
274
275    async fn save_rewrite_rules_state(
276        &mut self,
277        state: &RewriteRulesState,
278    ) -> Result<(), Self::Error>;
279    async fn load_rewrite_rules_state(&self) -> Result<RewriteRulesState, Self::Error>;
280
281    async fn save_pending_edits_state(
282        &mut self,
283        state: &PendingEditsState,
284    ) -> Result<(), Self::Error>;
285    async fn load_pending_edits_state(&self) -> Result<PendingEditsState, Self::Error>;
286
287    async fn save_pending_rewrite_rules_state(
288        &mut self,
289        state: &PendingRewriteRulesState,
290    ) -> Result<(), Self::Error>;
291    async fn load_pending_rewrite_rules_state(
292        &self,
293    ) -> Result<PendingRewriteRulesState, Self::Error>;
294
295    async fn save_settings_state(&mut self, state: &SettingsState) -> Result<(), Self::Error>;
296    async fn load_settings_state(&self) -> Result<SettingsState, Self::Error>;
297}
298
299// Re-export implementations
300#[cfg(feature = "pickledb")]
301mod file_storage;
302#[cfg(feature = "pickledb")]
303pub use file_storage::FileStorage;
304
305mod memory_storage;
306pub use memory_storage::MemoryStorage;