1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
pub mod delete;
pub mod edit;
pub mod list;
pub mod list_output;
pub mod search;
pub mod search_output;
pub mod show;
pub mod show_output;
pub mod utils;
use clap::{Subcommand, ValueEnum};
use lastfm_edit::LastFmEditClientImpl;
#[derive(ValueEnum, Clone)]
pub enum SearchType {
/// Search for tracks
Tracks,
/// Search for albums
Albums,
/// Search for artists
Artists,
}
#[derive(Subcommand)]
pub enum ListCommands {
/// List all artists in your library
///
/// This command lists all artists in your Last.fm library, sorted by play count
/// (highest first). Outputs JSON with artist names and scrobble counts.
///
/// Usage examples:
/// # List all artists
/// lastfm-edit list artists
///
/// # List first 20 artists
/// lastfm-edit list artists --limit 20
Artists {
/// Maximum number of artists to show (0 for no limit)
#[arg(long, default_value = "0")]
limit: usize,
},
/// List albums for an artist
///
/// This command lists all albums in your library for a specified artist.
/// The albums are sorted by play count (highest first). Outputs JSON.
///
/// Usage examples:
/// # List all albums for The Beatles
/// lastfm-edit list albums "The Beatles"
///
/// # List first 10 albums
/// lastfm-edit list albums "Radiohead" --limit 10
Albums {
/// Artist name
artist: String,
/// Maximum number of albums to show (0 for no limit)
#[arg(long, default_value = "0")]
limit: usize,
},
/// List all tracks for an artist with album information (album-based iteration)
///
/// This command lists all tracks in your library for a specified artist,
/// with complete album information included. Unlike tracks-by-album, this
/// shows tracks in a flat list with album details for each track.
/// Note: This uses album-based iteration, so tracks without album metadata may be missed.
///
/// Usage examples:
/// # List all tracks for The Beatles with album info
/// lastfm-edit list tracks "The Beatles"
///
/// # List first 20 tracks
/// lastfm-edit list tracks "Radiohead" --limit 20
Tracks {
/// Artist name
artist: String,
/// Maximum number of tracks to show (0 for no limit)
#[arg(long, default_value = "0")]
limit: usize,
},
/// List all tracks for an artist using direct track iteration
///
/// This command lists all tracks in your library for a specified artist using
/// direct track iteration. This approach finds ALL tracks, including those
/// without album metadata (singles, B-sides, etc.) that may be missed by the
/// regular tracks command.
///
/// Usage examples:
/// # List all tracks including those without albums
/// lastfm-edit list tracks-direct "The Beatles"
///
/// # Compare with regular tracks command to find missing tracks
/// lastfm-edit list tracks-direct "The Beatles" --limit 20
TracksDirect {
/// Artist name
artist: String,
/// Maximum number of tracks to show (0 for no limit)
#[arg(long, default_value = "0")]
limit: usize,
},
/// List tracks organized by album for an artist
///
/// This command lists all tracks in your library for a specified artist,
/// organized by album. For each album, it shows all tracks from that album.
///
/// Usage examples:
/// # List all tracks by album for The Beatles
/// lastfm-edit list tracks-by-album "The Beatles"
///
/// # List tracks for first 5 albums
/// lastfm-edit list tracks-by-album "Pink Floyd" --limit 5
TracksByAlbum {
/// Artist name
artist: String,
/// Maximum number of albums to show (0 for no limit)
#[arg(long, default_value = "0")]
limit: usize,
},
/// List tracks for a specific album
///
/// This command lists all tracks for a specific album by a specific artist.
/// This is useful for albums with special characters like slashes in their names.
///
/// Usage examples:
/// # List all tracks for AC/DC's "Back in Black" album
/// lastfm-edit list album-tracks "Back in Black" "AC/DC"
AlbumTracks {
/// Album name
album: String,
/// Artist name
artist: String,
},
}
#[derive(Subcommand)]
pub enum Commands {
/// Edit scrobble metadata
///
/// This command allows you to edit scrobble metadata by specifying what to search for
/// and what to change it to. You can specify any combination of fields to search for,
/// and any combination of new values to change them to.
///
/// Usage examples:
/// # Discover variations for an artist (dry run by default)
/// lastfm-edit edit --artist "Jimi Hendrix"
///
/// # Discover variations with optional track name
/// lastfm-edit edit --artist "Radiohead" --track "Creep"
///
/// # Actually apply edits (change artist name)
/// lastfm-edit edit --artist "The Beatles" --new-artist "Beatles, The" --apply
///
/// # Change track name for specific track
/// lastfm-edit edit --artist "Jimi Hendrix" --track "Lover Man" --new-track "Lover Man (Live)" --apply
Edit {
/// Artist name (required)
#[arg(long)]
artist: String,
/// Track name (optional)
#[arg(long)]
track: Option<String>,
/// Album name (optional)
#[arg(long)]
album: Option<String>,
/// Album artist name (optional)
#[arg(long)]
album_artist: Option<String>,
/// New track name (optional)
#[arg(long)]
new_track: Option<String>,
/// New album name (optional)
#[arg(long)]
new_album: Option<String>,
/// New artist name (optional)
#[arg(long)]
new_artist: Option<String>,
/// New album artist name (optional)
#[arg(long)]
new_album_artist: Option<String>,
/// Timestamp for specific scrobble (optional)
#[arg(long)]
timestamp: Option<u64>,
/// Disable editing all instances (edit only specific scrobble, defaults to editing all)
#[arg(long)]
no_edit_all: bool,
/// Actually apply the edits (default is dry-run mode)
#[arg(long)]
apply: bool,
/// Perform a dry run without actually submitting edits (default behavior)
#[arg(long)]
dry_run: bool,
},
/// Delete scrobbles in a range
///
/// This command allows you to delete scrobbles from your library. You can specify
/// timestamp ranges, delete recent scrobbles from specific pages, or use offsets
/// from the most recent scrobble.
///
/// Usage examples:
/// # Show recent scrobbles that would be deleted (dry run)
/// lastfm-edit delete --recent-pages 1-3
///
/// # Delete scrobbles from timestamp range
/// lastfm-edit delete --timestamp-range 1640995200-1641000000 --apply
///
/// # Delete scrobbles by offset from most recent (0-indexed)
/// lastfm-edit delete --recent-offset 0-4 --apply
Delete {
/// Delete scrobbles from recent pages (format: start-end, 0-indexed)
#[arg(long, conflicts_with_all = ["timestamp_range", "recent_offset"])]
recent_pages: Option<String>,
/// Delete scrobbles from timestamp range (format: start_ts-end_ts)
#[arg(long, conflicts_with_all = ["recent_pages", "recent_offset"])]
timestamp_range: Option<String>,
/// Delete scrobbles by offset from most recent (format: start-end, 0-indexed)
#[arg(long, conflicts_with_all = ["recent_pages", "timestamp_range"])]
recent_offset: Option<String>,
/// Actually perform the deletions (default is dry-run mode)
#[arg(long)]
apply: bool,
/// Perform a dry run without actually deleting (default behavior)
#[arg(long)]
dry_run: bool,
},
/// Search tracks, albums, and artists in your library
///
/// This command allows you to search through your Last.fm library for tracks, albums,
/// or artists that match a specific query. You can limit the number of results and
/// specify the type of search. Outputs JSON.
///
/// Usage examples:
/// # Search for tracks containing "remaster"
/// lastfm-edit search tracks "remaster"
///
/// # Search for first 20 albums containing "deluxe"
/// lastfm-edit search albums "deluxe" --limit 20
///
/// # Search for artists matching "radio"
/// lastfm-edit search artists "radio" --limit 10
///
/// # Search for tracks with unlimited results
/// lastfm-edit search tracks "live" --limit 0
///
/// # Skip first 10 results and show next 20
/// lastfm-edit search tracks "live" --offset 10 --limit 20
Search {
/// Type of search: tracks, albums, or artists
#[arg(value_enum)]
search_type: SearchType,
/// Search query
query: String,
/// Maximum number of results to show (0 for no limit)
#[arg(long, default_value = "50")]
limit: usize,
/// Number of results to skip from the beginning (0-indexed)
#[arg(long, default_value = "0")]
offset: usize,
},
/// Show scrobble details for specific offsets
///
/// This command displays detailed information for scrobbles at the specified
/// offsets from your most recent scrobbles.
///
/// Usage examples:
/// # Show details for the most recent scrobble (offset 0)
/// lastfm-edit show 0
///
/// # Show details for multiple scrobbles (0-indexed)
/// lastfm-edit show 0 1 2 5 10
Show {
/// Offsets of scrobbles to show (0-indexed, 0 = most recent)
offsets: Vec<u64>,
},
/// List artists, albums, and tracks from your library
///
/// This command allows you to browse your Last.fm library by listing artists,
/// albums, and tracks.
///
/// Usage examples:
/// # List all artists in your library
/// lastfm-edit list artists --limit 20 --details
///
/// # List all albums for The Beatles
/// lastfm-edit list albums "The Beatles"
///
/// # List all tracks with album information
/// lastfm-edit list tracks "Radiohead" --limit 20 --details
///
/// # List tracks organized by album
/// lastfm-edit list tracks-by-album "Pink Floyd" --limit 5 --details
List {
#[command(subcommand)]
command: ListCommands,
},
}
/// Execute the appropriate command handler based on the parsed command
pub async fn execute_command(
command: Commands,
client: &LastFmEditClientImpl,
) -> Result<(), Box<dyn std::error::Error>> {
match command {
Commands::Edit {
artist,
track,
album,
album_artist,
new_track,
new_album,
new_artist,
new_album_artist,
timestamp,
no_edit_all,
apply,
dry_run,
} => {
// Determine whether this is a dry run or actual edit
let is_dry_run = dry_run || !apply;
let edit = edit::create_scrobble_edit_from_args(
&artist,
track.as_deref(),
album.as_deref(),
album_artist.as_deref(),
new_track.as_deref(),
new_album.as_deref(),
new_artist.as_deref(),
new_album_artist.as_deref(),
timestamp,
!no_edit_all, // edit_all is true by default, false only if --no-edit-all is provided
);
edit::handle_edit_command(client, &edit, is_dry_run).await
}
Commands::Delete {
recent_pages,
timestamp_range,
recent_offset,
apply,
dry_run,
} => {
// Determine whether this is a dry run or actual deletion
let is_dry_run = dry_run || !apply;
if let Some(pages_range) = recent_pages {
delete::handle_delete_recent_pages(client, &pages_range, is_dry_run).await
} else if let Some(ts_range) = timestamp_range {
delete::handle_delete_timestamp_range(client, &ts_range, is_dry_run).await
} else if let Some(offset_range) = recent_offset {
delete::handle_delete_recent_offset(client, &offset_range, is_dry_run).await
} else {
Err(
"Must specify one of: --recent-pages, --timestamp-range, or --recent-offset"
.into(),
)
}
}
Commands::Search {
search_type,
query,
limit,
offset,
} => search::handle_search_command(client, search_type, &query, limit, offset).await,
Commands::Show { offsets } => {
if offsets.is_empty() {
return Err("Must specify at least one offset to show".into());
}
show::handle_show_scrobbles(client, &offsets).await
}
Commands::List { command } => match command {
ListCommands::Artists { limit } => list::handle_list_artists(client, limit).await,
ListCommands::Albums { artist, limit } => {
list::handle_list_albums(client, &artist, limit).await
}
ListCommands::Tracks { artist, limit } => {
list::handle_list_tracks(client, &artist, limit).await
}
ListCommands::TracksDirect { artist, limit } => {
list::handle_list_tracks_direct(client, &artist, limit).await
}
ListCommands::TracksByAlbum { artist, limit } => {
list::handle_list_tracks_by_album(client, &artist, limit).await
}
ListCommands::AlbumTracks { album, artist } => {
list::handle_list_album_tracks(client, &album, &artist).await
}
},
}
}