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 commands;
217pub mod discovery;
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 session_persistence;
229pub mod track;
230pub mod r#trait;
231
232pub use album::{Album, AlbumPage};
233pub use client::LastFmEditClientImpl;
234pub use discovery::{
235 AlbumTracksDiscovery, ArtistTracksDiscovery, AsyncDiscoveryIterator, ExactMatchDiscovery,
236 TrackVariationsDiscovery,
237};
238pub use events::{
239 ClientEvent, ClientEventReceiver, ClientEventWatcher, RateLimitType, RequestInfo,
240};
241pub use login::LoginManager;
242pub use r#trait::LastFmEditClient;
243
244// Re-export the mock when the mock feature is enabled
245pub use edit::{EditResponse, ExactScrobbleEdit, ScrobbleEdit, SingleEditResponse};
246pub use error::LastFmError;
247pub use iterator::AsyncPaginatedIterator;
248pub use retry::{ClientConfig, RateLimitConfig, RetryConfig};
249
250// Type aliases for iterators with the concrete client type
251pub type ArtistTracksIterator = iterator::ArtistTracksIterator<LastFmEditClientImpl>;
252pub type ArtistAlbumsIterator = iterator::ArtistAlbumsIterator<LastFmEditClientImpl>;
253pub type AlbumTracksIterator = iterator::AlbumTracksIterator<LastFmEditClientImpl>;
254pub type RecentTracksIterator = iterator::RecentTracksIterator<LastFmEditClientImpl>;
255pub type SearchTracksIterator = iterator::SearchTracksIterator<LastFmEditClientImpl>;
256pub type SearchAlbumsIterator = iterator::SearchAlbumsIterator<LastFmEditClientImpl>;
257#[cfg(feature = "mock")]
258pub use r#trait::MockLastFmEditClient;
259
260// Re-export the mock iterator when the mock feature is enabled
261#[cfg(feature = "mock")]
262pub use iterator::MockAsyncPaginatedIterator;
263pub use session::LastFmEditSession;
264pub use session_persistence::{SessionManager, SessionPersistence};
265pub use track::{Track, TrackPage};
266
267// Re-export scraper types for testing
268pub use scraper::Html;
269
270/// A convenient type alias for [`Result`] with [`LastFmError`] as the error type.
271pub type Result<T> = std::result::Result<T, LastFmError>;