Big Debug of PMOQobuz step 2
This commit is contained in:
@@ -92,8 +92,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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)");
|
||||
|
||||
@@ -55,8 +55,9 @@ impl Spoofer {
|
||||
.await?;
|
||||
|
||||
// Extraire l'URL du bundle
|
||||
let bundle_url_regex =
|
||||
Regex::new(r#"<script src="(/resources/\d+\.\d+\.\d+-[a-z]\d{3}/bundle\.js)"></script>"#)?;
|
||||
let bundle_url_regex = Regex::new(
|
||||
r#"<script src="(/resources/\d+\.\d+\.\d+-[a-z]\d{3}/bundle\.js)"></script>"#,
|
||||
)?;
|
||||
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_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
|
||||
);
|
||||
}
|
||||
}
|
||||
eprintln!("Erreur UTF-8 pour timezone {}: {}", timezone, e);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Erreur de décodage base64 pour timezone {}: {}",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -38,13 +38,13 @@ pub struct QobuzApi {
|
||||
/// Client HTTP
|
||||
client: Client,
|
||||
/// App ID pour l'authentification
|
||||
app_id: String,
|
||||
app_id: RwLock<String>,
|
||||
/// 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<Vec<u8>>,
|
||||
secret: RwLock<Option<Vec<u8>>>,
|
||||
/// Token d'authentification utilisateur
|
||||
user_auth_token: RwLock<Option<String>>,
|
||||
/// 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<String>, configvalue: &str) -> Result<Self> {
|
||||
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<u8>) {
|
||||
self.secret = Some(secret);
|
||||
pub fn set_secret(&self, secret: Vec<u8>) {
|
||||
*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<Vec<u8>> {
|
||||
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<String>, 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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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,25 +218,33 @@ 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<QobuzApi> {
|
||||
match crate::api::Spoofer::new().await {
|
||||
Ok(spoofer) => {
|
||||
match spoofer.get_app_id() {
|
||||
Ok(app_id) => {
|
||||
info!("Spoofer found App ID: {}", app_id);
|
||||
if let Some((app_id, secret)) = Self::fetch_spoofer_credentials(config).await? {
|
||||
return QobuzApi::with_secret(app_id, &secret);
|
||||
}
|
||||
|
||||
match spoofer.get_secrets() {
|
||||
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<Option<(String, String)>> {
|
||||
match crate::api::Spoofer::new().await {
|
||||
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());
|
||||
|
||||
// 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(test_api) => {
|
||||
info!("✓ Successfully created API with secret from timezone: {}", timezone);
|
||||
Ok(_) => {
|
||||
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);
|
||||
}
|
||||
@@ -244,7 +252,7 @@ impl QobuzClient {
|
||||
debug!("Could not save secret to config: {}", e);
|
||||
}
|
||||
|
||||
return Ok(test_api);
|
||||
return Ok(Some((app_id.clone(), secret.clone())));
|
||||
}
|
||||
Err(e) => {
|
||||
debug!(
|
||||
@@ -256,35 +264,53 @@ impl QobuzClient {
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
info!("✗ No valid secret from Spoofer secrets list");
|
||||
Ok(None)
|
||||
}
|
||||
Err(e) => {
|
||||
info!("Spoofer failed to extract secrets: {}, falling back to DEFAULT_APP_ID", e);
|
||||
QobuzApi::new(DEFAULT_APP_ID)
|
||||
}
|
||||
}
|
||||
info!(
|
||||
"Spoofer failed to extract secrets: {}, falling back to DEFAULT_APP_ID",
|
||||
e
|
||||
);
|
||||
Ok(None)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
info!(
|
||||
"Spoofer failed to extract app_id: {}, falling back to DEFAULT_APP_ID",
|
||||
e
|
||||
);
|
||||
QobuzApi::new(DEFAULT_APP_ID)
|
||||
}
|
||||
}
|
||||
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<Vec<Album>> {
|
||||
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<Vec<Artist>> {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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<usize>;
|
||||
}
|
||||
@@ -105,9 +100,7 @@ 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 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<u8> = row.get(2)?;
|
||||
@@ -120,13 +113,8 @@ impl CacheStore for SqliteCacheStore {
|
||||
};
|
||||
let age = Duration::from_secs(age_secs);
|
||||
let value = serde_json::from_slice(&data)?;
|
||||
Ok(CacheEntry {
|
||||
value,
|
||||
age,
|
||||
fresh,
|
||||
})
|
||||
},
|
||||
);
|
||||
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();
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<usize> {
|
||||
use tracing::{info, warn, debug};
|
||||
pub async fn add_album_to_playlist(&self, playlist_id: &str, album_id: &str) -> Result<usize> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user