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}