use std::sync::Arc; use std::time::Duration; use tokio::sync::Mutex; use tokio::time::Instant; /// A simple rate limiter that enforces a minimum interval between requests. /// Can be cloned (via Arc) to share across multiple clients. #[derive(Clone)] pub struct RateLimiter { last_request: Arc>, interval: Duration, } impl RateLimiter { pub fn new(interval: Duration) -> Self { Self { last_request: Arc::new(Mutex::new(Instant::now() - interval)), interval, } } /// Wait if needed so we don't exceed the rate limit. pub async fn wait(&self) { let mut last = self.last_request.lock().await; let elapsed = last.elapsed(); if elapsed < self.interval { tokio::time::sleep(self.interval - elapsed).await; } *last = Instant::now(); } } /// Build a reqwest client with a custom user agent and timeout. pub fn build_client(user_agent: &str, timeout_secs: u64) -> reqwest::Result { reqwest::Client::builder() .user_agent(user_agent) .timeout(Duration::from_secs(timeout_secs)) .build() } /// Simple URL-encode for query parameters. pub fn urlencoded(s: &str) -> String { s.replace(' ', "+") .replace('&', "%26") .replace('=', "%3D") .replace('#', "%23") } /// Escape special characters for MusicBrainz Lucene query syntax. pub fn escape_lucene(s: &str) -> String { let special = [ '+', '-', '&', '|', '!', '(', ')', '{', '}', '[', ']', '^', '"', '~', '*', '?', ':', '\\', '/', ]; let mut result = String::with_capacity(s.len()); for c in s.chars() { if special.contains(&c) { result.push('\\'); } result.push(c); } result }