lastfm_edit/
lib.rs

1//! # lastfm-edit
2//!
3//! A Rust crate for programmatic access to Last.fm's scrobble editing functionality via web scraping.
4//!
5//! This crate provides a high-level interface for authenticating with Last.fm, browsing user libraries,
6//! and performing bulk edits on scrobbled tracks. It uses web scraping to access functionality not
7//! available through Last.fm's public API.
8//!
9//! ## Features
10//!
11//! - **Authentication**: Login to Last.fm with username/password
12//! - **Library browsing**: Paginated access to tracks, albums, and recent scrobbles
13//! - **Bulk editing**: Edit track names, artist names, and album information
14//! - **Async iterators**: Stream large datasets efficiently
15//! - **HTTP client abstraction**: Works with any HTTP client implementation
16//!
17//! ## Quick Start
18//!
19//! ```rust,no_run
20//! use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, AsyncPaginatedIterator, Result};
21//!
22//! #[tokio::main]
23//! async fn main() -> Result<()> {
24//!     // Create HTTP client and login
25//!     let http_client = http_client::native::NativeClient::new();
26//!     let client = LastFmEditClientImpl::login_with_credentials(
27//!         Box::new(http_client),
28//!         "username",
29//!         "password"
30//!     ).await?;
31//!
32//!     // Browse recent tracks
33//!     let mut recent_tracks = client.recent_tracks();
34//!     while let Some(track) = recent_tracks.next().await? {
35//!         println!("{} - {}", track.artist, track.name);
36//!     }
37//!
38//!     Ok(())
39//! }
40//! ```
41//!
42//! ## Core Components
43//!
44//! - [`LastFmEditClient`] - Main client trait for interacting with Last.fm
45//! - [`Track`], [`Album`] - Data structures for music metadata
46//! - [`AsyncPaginatedIterator`] - Trait for streaming paginated data
47//! - [`ScrobbleEdit`] - Represents track edit operations
48//! - [`LastFmError`] - Error types for the crate
49//!
50//! ## Installation
51//!
52//! Add this to your `Cargo.toml`:
53//! ```toml
54//! [dependencies]
55//! lastfm-edit = "3.1.0"
56//! http-client = { version = "^6.6.3", package = "http-client-2", features = ["curl_client"] }
57//! tokio = { version = "1.0", features = ["full"] }
58//! ```
59//!
60//! ## Usage Patterns
61//!
62//! ### Basic Library Browsing
63//!
64//! ```rust,no_run
65//! use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, AsyncPaginatedIterator, Result};
66//!
67//! #[tokio::main]
68//! async fn main() -> Result<()> {
69//!     let http_client = http_client::native::NativeClient::new();
70//!     let client = LastFmEditClientImpl::login_with_credentials(
71//!         Box::new(http_client),
72//!         "username",
73//!         "password"
74//!     ).await?;
75//!
76//!     // Get all tracks by an artist
77//!     let mut tracks = client.artist_tracks("Radiohead");
78//!     while let Some(track) = tracks.next().await? {
79//!         println!("{} - {}", track.artist, track.name);
80//!     }
81//!
82//!     Ok(())
83//! }
84//! ```
85//!
86//! ### Bulk Track Editing
87//!
88//! ```rust,no_run
89//! use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, ScrobbleEdit, AsyncPaginatedIterator, Result};
90//!
91//! #[tokio::main]
92//! async fn main() -> Result<()> {
93//!     let http_client = http_client::native::NativeClient::new();
94//!     let client = LastFmEditClientImpl::login_with_credentials(
95//!         Box::new(http_client),
96//!         "username",
97//!         "password"
98//!     ).await?;
99//!
100//!     // Find and edit tracks
101//!     let tracks = client.artist_tracks("Artist Name").collect_all().await?;
102//!     for track in tracks {
103//!         if track.name.contains("(Remaster)") {
104//!             let new_name = track.name.replace(" (Remaster)", "");
105//!
106//!             // Create edit for this track
107//!             let edit = ScrobbleEdit::from_track_info(
108//!                 &track.name,
109//!                 &track.name, // Use track name as album fallback
110//!                 &track.artist,
111//!                 0 // No timestamp needed for bulk edit
112//!             )
113//!             .with_track_name(&new_name)
114//!             .with_edit_all(true);
115//!
116//!             let response = client.edit_scrobble(&edit).await?;
117//!             if response.success() {
118//!                 println!("Successfully edited: {} -> {}", track.name, new_name);
119//!             }
120//!         }
121//!     }
122//!
123//!     Ok(())
124//! }
125//! ```
126//!
127//! ### Recent Tracks Monitoring
128//!
129//! ```rust,no_run
130//! use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, AsyncPaginatedIterator, Result};
131//!
132//! #[tokio::main]
133//! async fn main() -> Result<()> {
134//!     let http_client = http_client::native::NativeClient::new();
135//!     let client = LastFmEditClientImpl::login_with_credentials(
136//!         Box::new(http_client),
137//!         "username",
138//!         "password"
139//!     ).await?;
140//!
141//!     // Get recent tracks (first 100)
142//!     let recent_tracks = client.recent_tracks().take(100).await?;
143//!     println!("Found {} recent tracks", recent_tracks.len());
144//!
145//!     Ok(())
146//! }
147//! ```
148//!
149//! ### Mocking for Testing
150//!
151//! Enable the `mock` feature to use `MockLastFmEditClient` for testing:
152//!
153//! ```toml
154//! [dev-dependencies]
155//! lastfm-edit = { version = "3.1.0", features = ["mock"] }
156//! mockall = "0.13"
157//! ```
158//!
159//! ```rust,ignore
160//! #[cfg(feature = "mock")]
161//! mod tests {
162//!     use lastfm_edit::{LastFmEditClient, MockLastFmEditClient, Result, EditResponse, ScrobbleEdit};
163//!     use mockall::predicate::*;
164//!
165//!     #[tokio::test]
166//!     async fn test_edit_workflow() -> Result<()> {
167//!         let mut mock_client = MockLastFmEditClient::new();
168//!
169//!         // Set up expectations
170//!         mock_client
171//!             .expect_login()
172//!             .with(eq("testuser"), eq("testpass"))
173//!             .times(1)
174//!             .returning(|_, _| Ok(()));
175//!
176//!         mock_client
177//!             .expect_edit_scrobble()
178//!             .times(1)
179//!             .returning(|_| Ok(EditResponse {
180//!                 success: true,
181//!                 message: Some("Edit successful".to_string()),
182//!             }));
183//!
184//!         // Use as trait object
185//!         let client: &dyn LastFmEditClient = &mock_client;
186//!
187//!         client.login("testuser", "testpass").await?;
188//!
189//!         let edit = ScrobbleEdit::new(
190//!             Some("Old Track".to_string()),
191//!             Some("Old Album".to_string()),
192//!             Some("Old Artist".to_string()),
193//!             Some("Old Artist".to_string()),
194//!             "New Track".to_string(),
195//!             "New Album".to_string(),
196//!             "New Artist".to_string(),
197//!             "New Artist".to_string(),
198//!             1640995200,
199//!             false,
200//!         );
201//!
202//!         let response = client.edit_scrobble(&edit).await?;
203//!         assert!(response.success);
204//!
205//!         Ok(())
206//!     }
207//! }
208//! ```
209//!
210//! ## License
211//!
212//! MIT
213
214pub mod album;
215pub mod client;
216pub mod discovery;
217pub mod discovery_iterator;
218pub mod edit;
219pub mod edit_analysis;
220pub mod error;
221pub mod events;
222pub mod headers;
223pub mod iterator;
224pub mod login;
225pub mod parsing;
226pub mod retry;
227pub mod session;
228pub mod track;
229pub mod r#trait;
230
231pub use album::{Album, AlbumPage};
232pub use client::LastFmEditClientImpl;
233pub use discovery::{
234    AlbumTracksDiscovery, ArtistTracksDiscovery, ExactMatchDiscovery, TrackVariationsDiscovery,
235};
236pub use discovery_iterator::AsyncDiscoveryIterator;
237pub use events::{
238    ClientEvent, ClientEventReceiver, ClientEventWatcher, RateLimitType, RequestInfo,
239};
240pub use login::LoginManager;
241pub use r#trait::LastFmEditClient;
242
243// Re-export the mock when the mock feature is enabled
244pub use edit::{EditResponse, ExactScrobbleEdit, ScrobbleEdit, SingleEditResponse};
245pub use error::LastFmError;
246pub use iterator::AsyncPaginatedIterator;
247
248// Type aliases for iterators with the concrete client type
249pub type ArtistTracksIterator = iterator::ArtistTracksIterator<LastFmEditClientImpl>;
250pub type ArtistAlbumsIterator = iterator::ArtistAlbumsIterator<LastFmEditClientImpl>;
251pub type AlbumTracksIterator = iterator::AlbumTracksIterator<LastFmEditClientImpl>;
252pub type RecentTracksIterator = iterator::RecentTracksIterator<LastFmEditClientImpl>;
253#[cfg(feature = "mock")]
254pub use r#trait::MockLastFmEditClient;
255
256// Re-export the mock iterator when the mock feature is enabled
257#[cfg(feature = "mock")]
258pub use iterator::MockAsyncPaginatedIterator;
259pub use session::LastFmEditSession;
260pub use track::{Track, TrackPage};
261
262// Re-export scraper types for testing
263pub use scraper::Html;
264
265/// A convenient type alias for [`Result`] with [`LastFmError`] as the error type.
266pub type Result<T> = std::result::Result<T, LastFmError>;