lastfm_edit/edit.rs
1/// Represents a scrobble edit operation.
2///
3/// This structure contains all the information needed to edit a specific scrobble
4/// on Last.fm, including both the original and new metadata values.
5///
6/// # Examples
7///
8/// ```rust
9/// use lastfm_edit::ScrobbleEdit;
10///
11/// // Create an edit to fix a track name
12/// let edit = ScrobbleEdit::from_track_info(
13/// "Paranoid Andriod", // original (misspelled)
14/// "OK Computer",
15/// "Radiohead",
16/// 1640995200
17/// )
18/// .with_track_name("Paranoid Android"); // corrected
19///
20/// // Create an edit to change artist name
21/// let edit = ScrobbleEdit::from_track_info(
22/// "Creep",
23/// "Pablo Honey",
24/// "Radio Head", // original (wrong)
25/// 1640995200
26/// )
27/// .with_artist_name("Radiohead") // corrected
28/// .with_edit_all(true); // update all instances
29/// ```
30#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
31pub struct ScrobbleEdit {
32 /// Original track name as it appears in the scrobble (optional - if None, edits all tracks)
33 pub track_name_original: Option<String>,
34 /// Original album name as it appears in the scrobble (optional)
35 pub album_name_original: Option<String>,
36 /// Original artist name as it appears in the scrobble (required)
37 pub artist_name_original: String,
38 /// Original album artist name as it appears in the scrobble (optional)
39 pub album_artist_name_original: Option<String>,
40
41 /// New track name to set (optional - if None, keeps original track names)
42 pub track_name: Option<String>,
43 /// New album name to set (optional - if None, keeps original album names)
44 pub album_name: Option<String>,
45 /// New artist name to set
46 pub artist_name: String,
47 /// New album artist name to set (optional - if None, keeps original album artist names)
48 pub album_artist_name: Option<String>,
49
50 /// Unix timestamp of the scrobble to edit (optional)
51 ///
52 /// This identifies the specific scrobble instance to modify.
53 /// If None, the client will attempt to find a representative timestamp.
54 pub timestamp: Option<u64>,
55 /// Whether to edit all instances or just this specific scrobble
56 ///
57 /// When `true`, Last.fm will update all scrobbles with matching metadata.
58 /// When `false`, only this specific scrobble (identified by timestamp) is updated.
59 pub edit_all: bool,
60}
61
62/// Response from a single scrobble edit operation.
63///
64/// This structure contains the result of attempting to edit a specific scrobble instance,
65/// including success status and any error messages.
66#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
67pub struct SingleEditResponse {
68 /// Whether this individual edit operation was successful
69 pub success: bool,
70 /// Optional message describing the result or any errors
71 pub message: Option<String>,
72 /// Information about which album variation was edited
73 pub album_info: Option<String>,
74 /// The exact scrobble edit that was performed
75 pub exact_scrobble_edit: ExactScrobbleEdit,
76}
77
78/// Response from a scrobble edit operation that may affect multiple album variations.
79///
80/// When editing a track that appears on multiple albums, this response contains
81/// the results of all individual edit operations performed.
82///
83/// # Examples
84///
85/// ```rust
86/// use lastfm_edit::{EditResponse, SingleEditResponse};
87///
88/// let response = EditResponse::from_results(vec![
89/// SingleEditResponse {
90/// success: true,
91/// message: Some("Edit successful".to_string()),
92/// album_info: Some("Album 1".to_string()),
93/// }
94/// ]);
95///
96/// // Check if all edits succeeded
97/// if response.all_successful() {
98/// println!("All {} edits succeeded!", response.total_edits());
99/// } else {
100/// println!("Some edits failed: {}", response.summary_message());
101/// }
102/// ```
103#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
104pub struct EditResponse {
105 /// Results of individual edit operations
106 pub individual_results: Vec<SingleEditResponse>,
107}
108
109/// Internal representation of a scrobble edit with all fields fully specified.
110///
111/// This type is used internally by the client after enriching metadata from
112/// Last.fm. Unlike `ScrobbleEdit`, all fields are required and non-optional,
113/// ensuring we have complete information before performing edit operations.
114///
115/// This type represents a fully-specified scrobble edit where all fields are known.
116#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
117pub struct ExactScrobbleEdit {
118 /// Original track name as it appears in the scrobble
119 pub track_name_original: String,
120 /// Original album name as it appears in the scrobble
121 pub album_name_original: String,
122 /// Original artist name as it appears in the scrobble
123 pub artist_name_original: String,
124 /// Original album artist name as it appears in the scrobble
125 pub album_artist_name_original: String,
126
127 /// New track name to set
128 pub track_name: String,
129 /// New album name to set
130 pub album_name: String,
131 /// New artist name to set
132 pub artist_name: String,
133 /// New album artist name to set
134 pub album_artist_name: String,
135
136 /// Unix timestamp of the scrobble to edit
137 pub timestamp: u64,
138 /// Whether to edit all instances or just this specific scrobble
139 pub edit_all: bool,
140}
141
142impl ScrobbleEdit {
143 /// Create a new [`ScrobbleEdit`] with all required fields.
144 ///
145 /// This is the most general constructor that allows setting all fields.
146 /// For convenience, consider using [`from_track_info`](Self::from_track_info) instead.
147 ///
148 /// # Arguments
149 ///
150 /// * `track_name_original` - The current track name in the scrobble
151 /// * `album_name_original` - The current album name in the scrobble
152 /// * `artist_name_original` - The current artist name in the scrobble
153 /// * `album_artist_name_original` - The current album artist name in the scrobble
154 /// * `track_name` - The new track name to set
155 /// * `album_name` - The new album name to set
156 /// * `artist_name` - The new artist name to set
157 /// * `album_artist_name` - The new album artist name to set
158 /// * `timestamp` - Unix timestamp identifying the scrobble
159 /// * `edit_all` - Whether to edit all matching scrobbles or just this one
160 #[allow(clippy::too_many_arguments)]
161 pub fn new(
162 track_name_original: Option<String>,
163 album_name_original: Option<String>,
164 artist_name_original: String,
165 album_artist_name_original: Option<String>,
166 track_name: Option<String>,
167 album_name: Option<String>,
168 artist_name: String,
169 album_artist_name: Option<String>,
170 timestamp: Option<u64>,
171 edit_all: bool,
172 ) -> Self {
173 Self {
174 track_name_original,
175 album_name_original,
176 artist_name_original,
177 album_artist_name_original,
178 track_name,
179 album_name,
180 artist_name,
181 album_artist_name,
182 timestamp,
183 edit_all,
184 }
185 }
186
187 /// Create an edit request from track information (convenience constructor).
188 ///
189 /// This constructor creates a [`ScrobbleEdit`] with the new values initially
190 /// set to the same as the original values. Use the builder methods like
191 /// [`with_track_name`](Self::with_track_name) to specify what should be changed.
192 ///
193 /// # Arguments
194 ///
195 /// * `original_track` - The current track name
196 /// * `original_album` - The current album name
197 /// * `original_artist` - The current artist name
198 /// * `timestamp` - Unix timestamp identifying the scrobble
199 ///
200 /// # Examples
201 ///
202 /// ```rust
203 /// use lastfm_edit::ScrobbleEdit;
204 ///
205 /// let edit = ScrobbleEdit::from_track_info(
206 /// "Highway to Hell",
207 /// "Highway to Hell",
208 /// "AC/DC",
209 /// 1640995200
210 /// )
211 /// .with_track_name("Highway to Hell (Remastered)");
212 /// ```
213 pub fn from_track_info(
214 original_track: &str,
215 original_album: &str,
216 original_artist: &str,
217 timestamp: u64,
218 ) -> Self {
219 Self::new(
220 Some(original_track.to_string()),
221 Some(original_album.to_string()),
222 original_artist.to_string(),
223 Some(original_artist.to_string()), // album_artist defaults to artist
224 Some(original_track.to_string()),
225 Some(original_album.to_string()),
226 original_artist.to_string(),
227 Some(original_artist.to_string()), // album_artist defaults to artist
228 Some(timestamp),
229 false, // edit_all defaults to false
230 )
231 }
232
233 /// Set the new track name.
234 ///
235 /// # Examples
236 ///
237 /// ```rust
238 /// # use lastfm_edit::ScrobbleEdit;
239 /// let edit = ScrobbleEdit::from_track_info("Wrong Name", "Album", "Artist", 1640995200)
240 /// .with_track_name("Correct Name");
241 /// ```
242 pub fn with_track_name(mut self, track_name: &str) -> Self {
243 self.track_name = Some(track_name.to_string());
244 self
245 }
246
247 /// Set the new album name.
248 ///
249 /// # Examples
250 ///
251 /// ```rust
252 /// # use lastfm_edit::ScrobbleEdit;
253 /// let edit = ScrobbleEdit::from_track_info("Track", "Wrong Album", "Artist", 1640995200)
254 /// .with_album_name("Correct Album");
255 /// ```
256 pub fn with_album_name(mut self, album_name: &str) -> Self {
257 self.album_name = Some(album_name.to_string());
258 self
259 }
260
261 /// Set the new artist name.
262 ///
263 /// This also sets the album artist name to the same value.
264 ///
265 /// # Examples
266 ///
267 /// ```rust
268 /// # use lastfm_edit::ScrobbleEdit;
269 /// let edit = ScrobbleEdit::from_track_info("Track", "Album", "Wrong Artist", 1640995200)
270 /// .with_artist_name("Correct Artist");
271 /// ```
272 pub fn with_artist_name(mut self, artist_name: &str) -> Self {
273 self.artist_name = artist_name.to_string();
274 self.album_artist_name = Some(artist_name.to_string());
275 self
276 }
277
278 /// Set whether to edit all instances of this track.
279 ///
280 /// When `true`, Last.fm will update all scrobbles with the same metadata.
281 /// When `false` (default), only the specific scrobble is updated.
282 ///
283 /// # Examples
284 ///
285 /// ```rust
286 /// # use lastfm_edit::ScrobbleEdit;
287 /// let edit = ScrobbleEdit::from_track_info("Track", "Album", "Artist", 1640995200)
288 /// .with_track_name("New Name")
289 /// .with_edit_all(true); // Update all instances
290 /// ```
291 pub fn with_edit_all(mut self, edit_all: bool) -> Self {
292 self.edit_all = edit_all;
293 self
294 }
295
296 /// Create an edit request with minimal information, letting the client look up missing metadata.
297 ///
298 /// This constructor is useful when you only know some of the original metadata and want
299 /// the client to automatically fill in missing information by looking up the scrobble.
300 ///
301 /// # Arguments
302 ///
303 /// * `track_name` - The new track name to set
304 /// * `artist_name` - The new artist name to set
305 /// * `album_name` - The new album name to set
306 /// * `timestamp` - Unix timestamp identifying the scrobble
307 ///
308 /// # Examples
309 ///
310 /// ```rust
311 /// use lastfm_edit::ScrobbleEdit;
312 ///
313 /// // Create an edit where the client will look up original metadata
314 /// let edit = ScrobbleEdit::with_minimal_info(
315 /// "Corrected Track Name",
316 /// "Corrected Artist",
317 /// "Corrected Album",
318 /// 1640995200
319 /// );
320 /// ```
321 pub fn with_minimal_info(
322 track_name: &str,
323 artist_name: &str,
324 album_name: &str,
325 timestamp: u64,
326 ) -> Self {
327 Self::new(
328 Some(track_name.to_string()),
329 Some(album_name.to_string()),
330 artist_name.to_string(),
331 Some(artist_name.to_string()),
332 Some(track_name.to_string()),
333 Some(album_name.to_string()),
334 artist_name.to_string(),
335 Some(artist_name.to_string()),
336 Some(timestamp),
337 false,
338 )
339 }
340 /// Create an edit request with just track and artist information.
341 ///
342 /// This constructor is useful when you only know the track and artist names.
343 /// The client will use these as both original and new values, and will
344 /// attempt to find a representative timestamp and album information.
345 ///
346 /// # Arguments
347 ///
348 /// * `track_name` - The track name (used as both original and new)
349 /// * `artist_name` - The artist name (used as both original and new)
350 ///
351 /// # Examples
352 ///
353 /// ```rust
354 /// use lastfm_edit::ScrobbleEdit;
355 ///
356 /// // Create an edit where the client will look up album and timestamp info
357 /// let edit = ScrobbleEdit::from_track_and_artist(
358 /// "Lover Man",
359 /// "Jimi Hendrix"
360 /// );
361 /// ```
362 pub fn from_track_and_artist(track_name: &str, artist_name: &str) -> Self {
363 Self::new(
364 Some(track_name.to_string()),
365 None, // Client will look up original album name
366 artist_name.to_string(),
367 None, // Client will look up original album artist name
368 Some(track_name.to_string()),
369 None, // Will be filled by client or kept as original
370 artist_name.to_string(),
371 Some(artist_name.to_string()), // album_artist defaults to artist
372 None, // Client will find representative timestamp
373 false,
374 )
375 }
376
377 /// Create an edit request for all tracks by an artist.
378 ///
379 /// This constructor creates a [`ScrobbleEdit`] that will edit all tracks
380 /// by the specified artist, changing the artist name to the new value.
381 ///
382 /// # Arguments
383 ///
384 /// * `old_artist_name` - The current artist name to change from
385 /// * `new_artist_name` - The new artist name to change to
386 ///
387 /// # Examples
388 ///
389 /// ```rust
390 /// use lastfm_edit::ScrobbleEdit;
391 ///
392 /// // Edit all tracks by "Radio Head" to "Radiohead"
393 /// let edit = ScrobbleEdit::for_artist("Radio Head", "Radiohead");
394 /// ```
395 pub fn for_artist(old_artist_name: &str, new_artist_name: &str) -> Self {
396 Self::new(
397 None, // No specific track - edit all tracks
398 None, // No specific album - edit all albums
399 old_artist_name.to_string(),
400 None, // Client will look up original album artist name
401 None, // No track name change - keep original track names
402 None, // Keep original album names (they can vary)
403 new_artist_name.to_string(),
404 Some(new_artist_name.to_string()), // album_artist also changes for global renames
405 None, // Client will find representative timestamp
406 true, // Edit all instances by default for artist changes
407 )
408 }
409
410 /// Create an edit request for all tracks in a specific album.
411 ///
412 /// This constructor creates a [`ScrobbleEdit`] that will edit all tracks
413 /// in the specified album by the specified artist.
414 ///
415 /// # Arguments
416 ///
417 /// * `album_name` - The album name containing tracks to edit
418 /// * `artist_name` - The artist name for the album
419 /// * `new_artist_name` - The new artist name to change to
420 ///
421 /// # Examples
422 ///
423 /// ```rust
424 /// use lastfm_edit::ScrobbleEdit;
425 ///
426 /// // Edit all tracks in "OK Computer" by "Radio Head" to "Radiohead"
427 /// let edit = ScrobbleEdit::for_album("OK Computer", "Radio Head", "Radiohead");
428 /// ```
429 pub fn for_album(album_name: &str, old_artist_name: &str, new_artist_name: &str) -> Self {
430 Self::new(
431 None, // No specific track - edit all tracks in album
432 Some(album_name.to_string()),
433 old_artist_name.to_string(),
434 Some(old_artist_name.to_string()),
435 None, // No track name change - keep original track names
436 Some(album_name.to_string()), // Keep same album name
437 new_artist_name.to_string(),
438 None, // Keep original album_artist names (they can vary)
439 None, // Client will find representative timestamp
440 true, // Edit all instances by default for album changes
441 )
442 }
443}
444
445impl ExactScrobbleEdit {
446 /// Create a new [`ExactScrobbleEdit`] with all fields specified.
447 #[allow(clippy::too_many_arguments)]
448 pub fn new(
449 track_name_original: String,
450 album_name_original: String,
451 artist_name_original: String,
452 album_artist_name_original: String,
453 track_name: String,
454 album_name: String,
455 artist_name: String,
456 album_artist_name: String,
457 timestamp: u64,
458 edit_all: bool,
459 ) -> Self {
460 Self {
461 track_name_original,
462 album_name_original,
463 artist_name_original,
464 album_artist_name_original,
465 track_name,
466 album_name,
467 artist_name,
468 album_artist_name,
469 timestamp,
470 edit_all,
471 }
472 }
473
474 /// Convert this exact edit back to a public ScrobbleEdit.
475 ///
476 /// This is useful when you need to expose the edit data through the public API.
477 pub fn to_scrobble_edit(&self) -> ScrobbleEdit {
478 ScrobbleEdit::new(
479 Some(self.track_name_original.clone()),
480 Some(self.album_name_original.clone()),
481 self.artist_name_original.clone(),
482 Some(self.album_artist_name_original.clone()),
483 Some(self.track_name.clone()),
484 Some(self.album_name.clone()),
485 self.artist_name.clone(),
486 Some(self.album_artist_name.clone()),
487 Some(self.timestamp),
488 self.edit_all,
489 )
490 }
491}
492
493impl EditResponse {
494 /// Create a new EditResponse from a single result.
495 pub fn single(
496 success: bool,
497 message: Option<String>,
498 album_info: Option<String>,
499 exact_scrobble_edit: ExactScrobbleEdit,
500 ) -> Self {
501 Self {
502 individual_results: vec![SingleEditResponse {
503 success,
504 message,
505 album_info,
506 exact_scrobble_edit,
507 }],
508 }
509 }
510
511 /// Create a new EditResponse from multiple results.
512 pub fn from_results(results: Vec<SingleEditResponse>) -> Self {
513 Self {
514 individual_results: results,
515 }
516 }
517
518 /// Check if all individual edit operations were successful.
519 pub fn all_successful(&self) -> bool {
520 !self.individual_results.is_empty() && self.individual_results.iter().all(|r| r.success)
521 }
522
523 /// Check if any individual edit operations were successful.
524 pub fn any_successful(&self) -> bool {
525 self.individual_results.iter().any(|r| r.success)
526 }
527
528 /// Get the total number of edit operations performed.
529 pub fn total_edits(&self) -> usize {
530 self.individual_results.len()
531 }
532
533 /// Get the number of successful edit operations.
534 pub fn successful_edits(&self) -> usize {
535 self.individual_results.iter().filter(|r| r.success).count()
536 }
537
538 /// Get the number of failed edit operations.
539 pub fn failed_edits(&self) -> usize {
540 self.individual_results
541 .iter()
542 .filter(|r| !r.success)
543 .count()
544 }
545
546 /// Generate a summary message describing the overall result.
547 pub fn summary_message(&self) -> String {
548 let total = self.total_edits();
549 let successful = self.successful_edits();
550 let failed = self.failed_edits();
551
552 if total == 0 {
553 return "No edit operations performed".to_string();
554 }
555
556 if successful == total {
557 if total == 1 {
558 "Edit completed successfully".to_string()
559 } else {
560 format!("All {total} edits completed successfully")
561 }
562 } else if successful == 0 {
563 if total == 1 {
564 "Edit failed".to_string()
565 } else {
566 format!("All {total} edits failed")
567 }
568 } else {
569 format!("{successful} of {total} edits succeeded, {failed} failed")
570 }
571 }
572
573 /// Get detailed messages from all edit operations.
574 pub fn detailed_messages(&self) -> Vec<String> {
575 self.individual_results
576 .iter()
577 .enumerate()
578 .map(|(i, result)| {
579 let album_info = result
580 .album_info
581 .as_deref()
582 .map(|info| format!(" ({info})"))
583 .unwrap_or_default();
584
585 match &result.message {
586 Some(msg) => format!("{}: {}{}", i + 1, msg, album_info),
587 None => {
588 if result.success {
589 format!("{}: Success{}", i + 1, album_info)
590 } else {
591 format!("{}: Failed{}", i + 1, album_info)
592 }
593 }
594 }
595 })
596 .collect()
597 }
598
599 /// Check if this response represents a single edit (for backward compatibility).
600 pub fn is_single_edit(&self) -> bool {
601 self.individual_results.len() == 1
602 }
603
604 /// Check if all edits succeeded (for backward compatibility).
605 pub fn success(&self) -> bool {
606 self.all_successful()
607 }
608
609 /// Get a single message for backward compatibility.
610 /// Returns the summary message.
611 pub fn message(&self) -> Option<String> {
612 Some(self.summary_message())
613 }
614}