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}