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