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