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>;