1use async_trait::async_trait;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use crate::rewrite::RewriteRule;
7
8#[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#[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 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 pub fn apply_rule_to_example(
161 &self,
162 ) -> Result<RuleTransformationPreview, crate::rewrite::RewriteError> {
163 let track = lastfm_edit::Track {
165 name: self.example_track_name.clone(),
166 artist: self.example_artist_name.clone(),
167 playcount: 1, timestamp: Some(1640995200), album: self.example_album_name.clone(),
170 album_artist: None, };
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 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 pub require_confirmation: bool,
261 pub require_confirmation_for_edits: bool,
263 pub require_confirmation_for_new_rules: bool,
265}
266
267#[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#[cfg(feature = "pickledb")]
301mod file_storage;
302#[cfg(feature = "pickledb")]
303pub use file_storage::FileStorage;
304
305mod memory_storage;
306pub use memory_storage::MemoryStorage;