diff --git a/pmoqobuz/examples/lazy_loading.rs b/pmoqobuz/examples/lazy_loading.rs index 985b507d..32aeb515 100644 --- a/pmoqobuz/examples/lazy_loading.rs +++ b/pmoqobuz/examples/lazy_loading.rs @@ -92,8 +92,15 @@ async fn main() -> Result<(), Box> { .await?; let elapsed = start.elapsed(); - println!("✅ Album added: {} tracks in {:.2}s", count, elapsed.as_secs_f64()); - println!(" Average: {:.0}ms per track\n", elapsed.as_millis() as f64 / count as f64); + println!( + "✅ Album added: {} tracks in {:.2}s", + count, + elapsed.as_secs_f64() + ); + println!( + " Average: {:.0}ms per track\n", + elapsed.as_millis() as f64 / count as f64 + ); // Step 8: Verify lazy PKs println!("🔍 Verifying lazy PKs..."); @@ -120,10 +127,20 @@ async fn main() -> Result<(), Box> { let is_lazy = pmocache::is_lazy_pk(&first_track_pk); println!(" First track PK: {}", first_track_pk); - println!(" Is lazy: {}", if is_lazy { "✅ YES (starts with 'L:')" } else { "❌ NO" }); + println!( + " Is lazy: {}", + if is_lazy { + "✅ YES (starts with 'L:')" + } else { + "❌ NO" + } + ); // Count lazy vs downloaded - let lazy_count = tracks.iter().filter(|t| pmocache::is_lazy_pk(t.cache_pk())).count(); + let lazy_count = tracks + .iter() + .filter(|t| pmocache::is_lazy_pk(t.cache_pk())) + .count(); let downloaded_count = tracks.len() - lazy_count; println!("\n📊 Track status:"); @@ -153,7 +170,10 @@ async fn main() -> Result<(), Box> { println!("│ 🎉 Lazy Loading Demo Complete! │"); println!("╰─────────────────────────────────────────╯"); println!("\n📈 Benefits demonstrated:"); - println!(" ✓ Fast album loading (~{}ms per track)", elapsed.as_millis() / count as u128); + println!( + " ✓ Fast album loading (~{}ms per track)", + elapsed.as_millis() / count as u128 + ); println!(" ✓ Minimal initial download (covers only)"); println!(" ✓ Audio downloaded on-demand"); println!(" ✓ Rate limiting active (respectful to Qobuz)"); diff --git a/pmoqobuz/examples/spoofer.rs b/pmoqobuz/examples/spoofer.rs index fa5a9f71..cccb9ed9 100644 --- a/pmoqobuz/examples/spoofer.rs +++ b/pmoqobuz/examples/spoofer.rs @@ -55,8 +55,9 @@ impl Spoofer { .await?; // Extraire l'URL du bundle - let bundle_url_regex = - Regex::new(r#""#)?; + let bundle_url_regex = Regex::new( + r#""#, + )?; let bundle_url = bundle_url_regex .captures(&login_page) .and_then(|cap| cap.get(1)) @@ -182,19 +183,14 @@ impl Spoofer { // Décoder en base64 match STANDARD.decode(trimmed) { - Ok(decoded_bytes) => { - match String::from_utf8(decoded_bytes) { - Ok(decoded_str) => { - decoded_secrets.insert(timezone, decoded_str); - } - Err(e) => { - eprintln!( - "Erreur UTF-8 pour timezone {}: {}", - timezone, e - ); - } + Ok(decoded_bytes) => match String::from_utf8(decoded_bytes) { + Ok(decoded_str) => { + decoded_secrets.insert(timezone, decoded_str); } - } + Err(e) => { + eprintln!("Erreur UTF-8 pour timezone {}: {}", timezone, e); + } + }, Err(e) => { eprintln!( "Erreur de décodage base64 pour timezone {}: {}", diff --git a/pmoqobuz/src/api/catalog.rs b/pmoqobuz/src/api/catalog.rs index 1614a226..d0c730f4 100644 --- a/pmoqobuz/src/api/catalog.rs +++ b/pmoqobuz/src/api/catalog.rs @@ -234,6 +234,10 @@ impl QobuzApi { let format_id = self.format_id.id().to_string(); let intent = "stream"; let timestamp = signing::get_timestamp(); + let app_id = self.app_id(); + let user_auth_token = self + .auth_token() + .ok_or_else(|| QobuzError::Unauthorized("Missing auth token".to_string()))?; // Signer la requête (comme Python: track_getFileUrl) let signature = @@ -251,6 +255,8 @@ impl QobuzApi { ("intent", intent), ("request_ts", timestamp.as_str()), ("request_sig", signature.as_str()), + ("app_id", app_id.as_str()), + ("user_auth_token", user_auth_token.as_str()), ]; // Utiliser GET (comme Python après sept 2024 selon le commentaire) diff --git a/pmoqobuz/src/api/mod.rs b/pmoqobuz/src/api/mod.rs index d65d8e12..863f50ba 100644 --- a/pmoqobuz/src/api/mod.rs +++ b/pmoqobuz/src/api/mod.rs @@ -38,13 +38,13 @@ pub struct QobuzApi { /// Client HTTP client: Client, /// App ID pour l'authentification - app_id: String, + app_id: RwLock, /// Secret s4 pour signer les requêtes sensibles (track/getFileUrl, userLibrary/*) /// /// Ce secret est obtenu soit : /// - En décodant un `configvalue` (base64) et XOR avec l'app_id /// - Depuis le Spoofer (secrets dynamiques) - secret: Option>, + secret: RwLock>>, /// Token d'authentification utilisateur user_auth_token: RwLock>, /// ID utilisateur @@ -65,8 +65,8 @@ impl QobuzApi { Ok(Self { client, - app_id: app_id.into(), - secret: None, + app_id: RwLock::new(app_id.into()), + secret: RwLock::new(None), user_auth_token: RwLock::new(None), user_id: RwLock::new(None), format_id: AudioFormat::default(), @@ -86,7 +86,7 @@ impl QobuzApi { /// Le configvalue est décodé depuis base64, puis XORé avec l'app_id /// pour obtenir le secret s4. pub fn with_secret(app_id: impl Into, configvalue: &str) -> Result { - let mut api = Self::new(app_id)?; + let api = Self::new(app_id)?; api.set_secret_from_configvalue(configvalue)?; Ok(api) } @@ -96,8 +96,8 @@ impl QobuzApi { /// # Arguments /// /// * `secret` - Secret s4 en bytes (déjà décodé et dérivé) - pub fn set_secret(&mut self, secret: Vec) { - self.secret = Some(secret); + pub fn set_secret(&self, secret: Vec) { + *self.secret.write().unwrap() = Some(secret); } /// Dérive et définit le secret s4 depuis un configvalue @@ -106,7 +106,7 @@ impl QobuzApi { /// 1. Décode le configvalue depuis base64 /// 2. XOR avec l'app_id /// 3. Stocke le résultat comme secret s4 - fn set_secret_from_configvalue(&mut self, configvalue: &str) -> Result<()> { + fn set_secret_from_configvalue(&self, configvalue: &str) -> Result<()> { use base64::{engine::general_purpose::STANDARD, Engine}; // Décoder le configvalue depuis base64 @@ -115,7 +115,8 @@ impl QobuzApi { .map_err(|e| QobuzError::Configuration(format!("Invalid configvalue: {}", e)))?; // XOR avec l'app_id - let app_id_bytes = self.app_id.as_bytes(); + let app_id = self.app_id.read().unwrap(); + let app_id_bytes = app_id.as_bytes(); let mut s4 = Vec::with_capacity(s3s.len()); for (i, &byte) in s3s.iter().enumerate() { @@ -123,13 +124,13 @@ impl QobuzApi { s4.push(byte ^ app_byte); } - self.secret = Some(s4); + *self.secret.write().unwrap() = Some(s4); Ok(()) } /// Retourne le secret s4 si disponible - pub fn secret(&self) -> Option<&[u8]> { - self.secret.as_deref() + pub fn secret(&self) -> Option> { + self.secret.read().unwrap().clone() } /// Définit le token d'authentification @@ -155,8 +156,18 @@ impl QobuzApi { } /// Retourne l'App ID - pub fn app_id(&self) -> &str { - &self.app_id + pub fn app_id(&self) -> String { + self.app_id.read().unwrap().clone() + } + + /// Met à jour dynamiquement l'app_id et le secret associés + pub fn update_credentials(&self, app_id: impl Into, configvalue: &str) -> Result<()> { + { + let mut current = self.app_id.write().unwrap(); + *current = app_id.into(); + } + + self.set_secret_from_configvalue(configvalue) } /// Retourne le token d'authentification si disponible @@ -205,7 +216,8 @@ impl QobuzApi { }; // Ajouter les headers - request = request.header("X-App-Id", &self.app_id); + let app_id = self.app_id.read().unwrap().clone(); + request = request.header("X-App-Id", app_id); if let Some(token) = self.auth_token() { request = request.header("X-User-Auth-Token", token); @@ -270,7 +282,7 @@ mod tests { #[test] fn test_api_creation() { let api = QobuzApi::new("test_app_id").unwrap(); - assert_eq!(api.app_id(), "test_app_id"); + assert_eq!(api.app_id(), "test_app_id".to_string()); assert!(api.auth_token().is_none()); } diff --git a/pmoqobuz/src/api/user.rs b/pmoqobuz/src/api/user.rs index 6858bb9f..6d0c80cf 100644 --- a/pmoqobuz/src/api/user.rs +++ b/pmoqobuz/src/api/user.rs @@ -234,10 +234,11 @@ impl QobuzApi { // Signer la requête (comme Python: userlib_getAlbums) let signature = signing::sign_userlib_get_albums(×tamp, &secret); + let app_id = self.app_id(); + debug!( "Signing userLibrary/getAlbumsList: app_id={}, ts={}", - self.app_id(), - timestamp + app_id, timestamp ); // Construire les paramètres signés @@ -246,7 +247,7 @@ impl QobuzApi { .ok_or_else(|| QobuzError::Unauthorized("No auth token".to_string()))?; let params = [ - ("app_id", self.app_id()), + ("app_id", app_id.as_str()), ("user_auth_token", user_auth_token.as_str()), ("request_ts", timestamp.as_str()), ("request_sig", signature.as_str()), diff --git a/pmoqobuz/src/client.rs b/pmoqobuz/src/client.rs index a68a3882..50446284 100644 --- a/pmoqobuz/src/client.rs +++ b/pmoqobuz/src/client.rs @@ -11,7 +11,7 @@ use crate::models::*; use pmoconfig::{self, Config}; use std::future::Future; use std::sync::{Arc, Mutex}; -use tracing::{debug, info}; +use tracing::{debug, info, warn}; /// Client Qobuz haut-niveau avec cache pub struct QobuzClient { @@ -218,73 +218,99 @@ impl QobuzClient { /// - Quand aucun appid/secret n'est configuré /// - Quand les credentials configurés sont invalides/expirés async fn try_spoofer_fallback(config: &Config) -> Result { + if let Some((app_id, secret)) = Self::fetch_spoofer_credentials(config).await? { + return QobuzApi::with_secret(app_id, &secret); + } + + info!( + "✗ No valid secret found from Spoofer, falling back to DEFAULT_APP_ID without secret" + ); + QobuzApi::new(DEFAULT_APP_ID) + } + + async fn fetch_spoofer_credentials(config: &Config) -> Result> { match crate::api::Spoofer::new().await { - Ok(spoofer) => { - match spoofer.get_app_id() { - Ok(app_id) => { - info!("Spoofer found App ID: {}", app_id); + Ok(spoofer) => match spoofer.get_app_id() { + Ok(app_id) => match spoofer.get_secrets() { + Ok(secrets) => { + info!("Spoofer found {} secret(s), testing them...", secrets.len()); - match spoofer.get_secrets() { - Ok(secrets) => { - info!("Spoofer found {} secret(s), testing them...", secrets.len()); + for (timezone, secret) in secrets.iter() { + debug!("Testing secret for timezone: {}", timezone); - // Tester chaque secret pour trouver celui qui fonctionne - for (timezone, secret) in secrets.iter() { - debug!("Testing secret for timezone: {}", timezone); + match QobuzApi::with_secret(&app_id, secret) { + Ok(_) => { + info!( + "✓ Successfully created API with secret from timezone: {}", + timezone + ); - match QobuzApi::with_secret(&app_id, secret) { - Ok(test_api) => { - info!("✓ Successfully created API with secret from timezone: {}", timezone); - - // Sauvegarder les credentials valides dans la config - if let Err(e) = config.set_qobuz_appid(&app_id) { - debug!("Could not save appid to config: {}", e); - } - if let Err(e) = config.set_qobuz_secret(secret) { - debug!("Could not save secret to config: {}", e); - } - - return Ok(test_api); - } - Err(e) => { - debug!( - "Failed to create API with secret from {}: {}", - timezone, e - ); - continue; - } + if let Err(e) = config.set_qobuz_appid(&app_id) { + debug!("Could not save appid to config: {}", e); + } + if let Err(e) = config.set_qobuz_secret(secret) { + debug!("Could not save secret to config: {}", e); } - } - // Si aucun secret n'a fonctionné, utiliser le fallback - info!("✗ No valid secret found from Spoofer, falling back to DEFAULT_APP_ID without secret"); - QobuzApi::new(DEFAULT_APP_ID) - } - Err(e) => { - info!("Spoofer failed to extract secrets: {}, falling back to DEFAULT_APP_ID", e); - QobuzApi::new(DEFAULT_APP_ID) + return Ok(Some((app_id.clone(), secret.clone()))); + } + Err(e) => { + debug!( + "Failed to create API with secret from {}: {}", + timezone, e + ); + continue; + } } } + + info!("✗ No valid secret from Spoofer secrets list"); + Ok(None) } Err(e) => { info!( - "Spoofer failed to extract app_id: {}, falling back to DEFAULT_APP_ID", + "Spoofer failed to extract secrets: {}, falling back to DEFAULT_APP_ID", e ); - QobuzApi::new(DEFAULT_APP_ID) + Ok(None) } + }, + Err(e) => { + info!( + "Spoofer failed to extract app_id: {}, falling back to DEFAULT_APP_ID", + e + ); + Ok(None) } - } + }, Err(e) => { info!( "Spoofer failed: {}, falling back to DEFAULT_APP_ID without secret", e ); - QobuzApi::new(DEFAULT_APP_ID) + Ok(None) } } } + async fn refresh_credentials_via_spoofer(&self) -> Result<()> { + let config_arc = match &self.config { + Some(cfg) => cfg.clone(), + None => pmoconfig::get_config(), + }; + + match Self::fetch_spoofer_credentials(config_arc.as_ref()).await? { + Some((app_id, secret)) => { + self.api.update_credentials(app_id, &secret)?; + info!("✓ Updated Qobuz API credentials using Spoofer"); + Ok(()) + } + None => Err(QobuzError::Configuration( + "Unable to refresh Qobuz credentials via Spoofer".to_string(), + )), + } + } + /// Définit le format audio par défaut pub fn set_format(&mut self, format: AudioFormat) { self.api.set_format(format); @@ -375,10 +401,7 @@ impl QobuzClient { match global.get_qobuz_cache_dir() { Ok(dir) => dir, Err(err) => { - debug!( - "Failed to read qobuz cache dir from global config: {}", - err - ); + debug!("Failed to read qobuz cache dir from global config: {}", err); return None; } } @@ -416,6 +439,25 @@ impl QobuzClient { { match op().await { Ok(result) => Ok(result), + Err(err) if err.is_signature_error() => { + warn!( + "Request signature error during {}. Qobuz app secret may be outdated.", + operation + ); + if let Err(refresh_err) = self.refresh_credentials_via_spoofer().await { + warn!( + "Failed to refresh Qobuz credentials automatically: {}", + refresh_err + ); + Err(err) + } else { + info!( + "Successfully refreshed Qobuz credentials. Retrying {}...", + operation + ); + op().await + } + } Err(err) if err.is_auth_error() => { info!( "Authentication error during {}. Attempting automatic repair...", @@ -577,13 +619,17 @@ impl QobuzClient { /// Récupère les albums d'un artiste pub async fn get_artist_albums(&self, artist_id: &str) -> Result> { - self.call_with_auth_repair("get_artist_albums", || self.api.get_artist_albums(artist_id)) + self.call_with_auth_repair("get_artist_albums", || { + self.api.get_artist_albums(artist_id) + }) .await } /// Récupère les artistes similaires pub async fn get_similar_artists(&self, artist_id: &str) -> Result> { - self.call_with_auth_repair("get_similar_artists", || self.api.get_similar_artists(artist_id)) + self.call_with_auth_repair("get_similar_artists", || { + self.api.get_similar_artists(artist_id) + }) .await } @@ -740,7 +786,7 @@ impl QobuzClient { /// Récupère les artistes favoris de l'utilisateur pub async fn get_favorite_artists(&self) -> Result> { self.call_with_auth_repair("get_favorite_artists", || self.api.get_favorite_artists()) - .await + .await } /// Récupère les tracks favorites de l'utilisateur diff --git a/pmoqobuz/src/config_ext.rs b/pmoqobuz/src/config_ext.rs index aacf9a27..fdf9025c 100644 --- a/pmoqobuz/src/config_ext.rs +++ b/pmoqobuz/src/config_ext.rs @@ -263,8 +263,8 @@ impl QobuzConfigExt for Config { match self.get_value(&["accounts", "qobuz", "appid"]) { Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)), Ok(Value::String(_)) => Ok(None), // Empty string - Ok(_) => Ok(None), // Wrong type - Err(_) => Ok(None), // Not configured + Ok(_) => Ok(None), // Wrong type + Err(_) => Ok(None), // Not configured } } @@ -279,8 +279,8 @@ impl QobuzConfigExt for Config { match self.get_value(&["accounts", "qobuz", "secret"]) { Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)), Ok(Value::String(_)) => Ok(None), // Empty string - Ok(_) => Ok(None), // Wrong type - Err(_) => Ok(None), // Not configured + Ok(_) => Ok(None), // Wrong type + Err(_) => Ok(None), // Not configured } } @@ -295,8 +295,8 @@ impl QobuzConfigExt for Config { match self.get_value(&["accounts", "qobuz", "auth_token"]) { Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)), Ok(Value::String(_)) => Ok(None), // Empty string - Ok(_) => Ok(None), // Wrong type - Err(_) => Ok(None), // Not configured + Ok(_) => Ok(None), // Wrong type + Err(_) => Ok(None), // Not configured } } @@ -304,8 +304,8 @@ impl QobuzConfigExt for Config { match self.get_value(&["accounts", "qobuz", "user_id"]) { Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)), Ok(Value::String(_)) => Ok(None), // Empty string - Ok(_) => Ok(None), // Wrong type - Err(_) => Ok(None), // Not configured + Ok(_) => Ok(None), // Wrong type + Err(_) => Ok(None), // Not configured } } @@ -322,8 +322,8 @@ impl QobuzConfigExt for Config { match self.get_value(&["accounts", "qobuz", "subscription_label"]) { Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)), Ok(Value::String(_)) => Ok(None), // Empty string - Ok(_) => Ok(None), // Wrong type - Err(_) => Ok(None), // Not configured + Ok(_) => Ok(None), // Wrong type + Err(_) => Ok(None), // Not configured } } @@ -359,8 +359,14 @@ impl QobuzConfigExt for Config { fn clear_qobuz_auth_info(&self) -> Result<()> { // On ne propage pas les erreurs car les valeurs peuvent ne pas exister - let _ = self.set_value(&["accounts", "qobuz", "auth_token"], Value::String(String::new())); - let _ = self.set_value(&["accounts", "qobuz", "user_id"], Value::String(String::new())); + let _ = self.set_value( + &["accounts", "qobuz", "auth_token"], + Value::String(String::new()), + ); + let _ = self.set_value( + &["accounts", "qobuz", "user_id"], + Value::String(String::new()), + ); let _ = self.set_value( &["accounts", "qobuz", "token_expires_at"], Value::Number(serde_yaml::Number::from(0)), diff --git a/pmoqobuz/src/disk_cache.rs b/pmoqobuz/src/disk_cache.rs index 033d7832..595d0abb 100644 --- a/pmoqobuz/src/disk_cache.rs +++ b/pmoqobuz/src/disk_cache.rs @@ -35,12 +35,7 @@ pub trait CacheStore: Send + Sync { value: &T, ) -> anyhow::Result<()>; - async fn invalidate( - &self, - user_id: &str, - namespace: &str, - key: &str, - ) -> anyhow::Result<()>; + async fn invalidate(&self, user_id: &str, namespace: &str, key: &str) -> anyhow::Result<()>; async fn purge_expired(&self) -> anyhow::Result; } @@ -105,28 +100,21 @@ impl CacheStore for SqliteCacheStore { WHERE user_id = ?1 AND namespace = ?2 AND key = ?3", )?; - let result = stmt.query_row( - params![user_id, namespace, key], - |row| { - let fetched_at: i64 = row.get(0)?; - let ttl_seconds: i64 = row.get(1)?; - let data: Vec = row.get(2)?; - let now = Self::now_seconds(); - let fresh = now <= fetched_at + ttl_seconds; - let age_secs = if now >= fetched_at { - (now - fetched_at) as u64 - } else { - 0 - }; - let age = Duration::from_secs(age_secs); - let value = serde_json::from_slice(&data)?; - Ok(CacheEntry { - value, - age, - fresh, - }) - }, - ); + let result = stmt.query_row(params![user_id, namespace, key], |row| { + let fetched_at: i64 = row.get(0)?; + let ttl_seconds: i64 = row.get(1)?; + let data: Vec = row.get(2)?; + let now = Self::now_seconds(); + let fresh = now <= fetched_at + ttl_seconds; + let age_secs = if now >= fetched_at { + (now - fetched_at) as u64 + } else { + 0 + }; + let age = Duration::from_secs(age_secs); + let value = serde_json::from_slice(&data)?; + Ok(CacheEntry { value, age, fresh }) + }); match result { Ok(entry) => Ok(Some(entry)), @@ -170,12 +158,7 @@ impl CacheStore for SqliteCacheStore { .map_err(|err| anyhow!(err))? } - async fn invalidate( - &self, - user_id: &str, - namespace: &str, - key: &str, - ) -> anyhow::Result<()> { + async fn invalidate(&self, user_id: &str, namespace: &str, key: &str) -> anyhow::Result<()> { let user_id = user_id.to_owned(); let namespace = namespace.to_owned(); let key = key.to_owned(); diff --git a/pmoqobuz/src/error.rs b/pmoqobuz/src/error.rs index baebe2eb..f5d0b174 100644 --- a/pmoqobuz/src/error.rs +++ b/pmoqobuz/src/error.rs @@ -80,8 +80,25 @@ impl QobuzError { pub fn is_auth_error(&self) -> bool { match self { QobuzError::Unauthorized(_) => true, + QobuzError::ApiError { + code: 401 | 403, .. + } => true, QobuzError::ApiError { code: 400, message } - if message.contains("app_id") || message.contains("Invalid") => true, + if message.contains("app_id") || message.contains("App ID") => + { + true + } + _ => false, + } + } + + /// Vérifie si l'erreur indique un problème de signature des requêtes + pub fn is_signature_error(&self) -> bool { + match self { + QobuzError::ApiError { code: 400, message } => { + let lowered = message.to_lowercase(); + lowered.contains("request_signature") || lowered.contains("request_sig") + } _ => false, } } diff --git a/pmoqobuz/src/source.rs b/pmoqobuz/src/source.rs index 58316f53..d4642f3a 100644 --- a/pmoqobuz/src/source.rs +++ b/pmoqobuz/src/source.rs @@ -318,12 +318,8 @@ impl QobuzSource { /// # Returns /// /// Number of tracks successfully added - pub async fn add_album_to_playlist( - &self, - playlist_id: &str, - album_id: &str, - ) -> Result { - use tracing::{info, warn, debug}; + pub async fn add_album_to_playlist(&self, playlist_id: &str, album_id: &str) -> Result { + use tracing::{debug, info, warn}; // 1. Get tracks from Qobuz (goes through rate limiter) let tracks = self @@ -360,12 +356,7 @@ impl QobuzSource { } } Err(e) => { - warn!( - "Failed to add track {} ({}): {}", - i + 1, - track.title, - e - ); + warn!("Failed to add track {} ({}): {}", i + 1, track.title, e); // Continue with other tracks } } @@ -451,12 +442,7 @@ impl QobuzSource { } } Err(e) => { - warn!( - "Failed to add track {} ({}): {}", - i + 1, - track.title, - e - ); + warn!("Failed to add track {} ({}): {}", i + 1, track.title, e); // Continue with other tracks } } diff --git a/pmoqobuz/tests/disk_cache.rs b/pmoqobuz/tests/disk_cache.rs index 9ba6ad36..87ddcbcc 100644 --- a/pmoqobuz/tests/disk_cache.rs +++ b/pmoqobuz/tests/disk_cache.rs @@ -12,7 +12,13 @@ async fn sqlite_cache_returns_fresh_entries() -> anyhow::Result<()> { let data = vec!["album".to_string()]; store - .put_json("user", "favorites_albums", "all", Duration::from_secs(3600), &data) + .put_json( + "user", + "favorites_albums", + "all", + Duration::from_secs(3600), + &data, + ) .await?; let entry = store @@ -34,7 +40,13 @@ async fn sqlite_cache_marks_entries_as_stale_after_ttl() -> anyhow::Result<()> { let data = vec!["track".to_string()]; store - .put_json("user", "favorites_tracks", "all", Duration::from_secs(1), &data) + .put_json( + "user", + "favorites_tracks", + "all", + Duration::from_secs(1), + &data, + ) .await?; sleep(Duration::from_secs(2)).await; @@ -57,7 +69,13 @@ async fn sqlite_cache_purge_expired_removes_entries() -> anyhow::Result<()> { let data = vec!["playlist".to_string()]; store - .put_json("user", "user_playlists", "all", Duration::from_secs(1), &data) + .put_json( + "user", + "user_playlists", + "all", + Duration::from_secs(1), + &data, + ) .await?; sleep(Duration::from_secs(2)).await;