diff --git a/pmoaudiocache/src/track_metadata.rs b/pmoaudiocache/src/track_metadata.rs index 2e454066..2831922a 100644 --- a/pmoaudiocache/src/track_metadata.rs +++ b/pmoaudiocache/src/track_metadata.rs @@ -35,6 +35,18 @@ impl AudioCacheTrackMetadata { } fn read_raw(&self, key: &str) -> Result, MetadataError> { + // Si le pk est un lazy pk déjà téléchargé (pk != lazy_pk dans l'asset), + // lire d'abord sous le real_pk (métadonnées écrites par FlacCacheSink), + // puis fallback sous le lazy_pk (cover_pk, qobuz_track_id semés à l'enregistrement). + if pmocache::is_lazy_pk(&self.pk) { + if let Ok(Some(real_pk)) = self.cache.db.get_pk_by_lazy_pk(&self.pk) { + if let Ok(Some(v)) = self.cache.db.get_a_metadata(&real_pk, key) { + return Ok(Some(v)); + } + // Fallback vers lazy_pk pour les clés semées avant téléchargement (cover_pk, etc.) + return self.cache.db.get_a_metadata(&self.pk, key).map_err(map_db_err); + } + } self.cache .db .get_a_metadata(&self.pk, key) diff --git a/pmocache/src/db.rs b/pmocache/src/db.rs index 2ce59015..4a49dc8d 100644 --- a/pmocache/src/db.rs +++ b/pmocache/src/db.rs @@ -1137,7 +1137,23 @@ impl DB { return Err(Error::QueryReturnedNoRows); } - tx.commit() + tx.commit()?; + + // Compter les métadonnées encore sous l'ancien lazy_pk (après migration de l'asset) + let meta_under_lazy: i64 = { + let conn = self.lock_conn("update_lazy_to_downloaded_meta_check"); + conn.query_row( + "SELECT COUNT(*) FROM metadata WHERE pk = ?1", + [lazy_pk], + |r| r.get(0), + ).unwrap_or(0) + }; + tracing::debug!( + "update_lazy_to_downloaded: {} → {} ({} metadata rows still under lazy_pk)", + lazy_pk, real_pk, meta_under_lazy + ); + + Ok(()) } /// Recherche une entry par son origin_url diff --git a/pmoqobuz/src/source.rs b/pmoqobuz/src/source.rs index b3cbb884..73a588ac 100644 --- a/pmoqobuz/src/source.rs +++ b/pmoqobuz/src/source.rs @@ -795,11 +795,19 @@ impl QobuzSource { .get_read_handle(&pmo_playlist_id) .await .map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?; - let (items, _total) = reader + let (items, total) = reader .to_items_paged(0, usize::MAX) .await .map_err(|e| MusicSourceError::PlaylistError(e.to_string()))?; - return self.adapt_items_to_qobuz(items, &parent_id).await; + // Si des PKs ne sont pas enregistrés dans le cache audio (e.g. playlist créée + // avec l'ancien code), on force un refresh pour les ré-enregistrer proprement. + if items.len() == total { + return self.adapt_items_to_qobuz(items, &parent_id).await; + } + info!( + "Qobuz playlist {} has {}/{} valid items, forcing refresh to repair missing entries", + pmo_playlist_id, items.len(), total + ); } info!("Qobuz playlist {} creating/refreshing (version {:?})", pmo_playlist_id, qobuz_version); @@ -1047,7 +1055,9 @@ impl QobuzSource { // 600 tracks ne génèrent que ~N_albums_uniques téléchargements réels. let sem = std::sync::Arc::new(tokio::sync::Semaphore::new(16)); - let futs: Vec<_> = tracks.iter().map(|track| { + // On attache l'index original à chaque future pour pouvoir retrier dans l'ordre + // d'origine après complétion parallèle (JoinSet retourne dans l'ordre de fin). + let futs: Vec<_> = tracks.iter().enumerate().map(|(idx, track)| { let source = self.clone(); let sem = sem.clone(); let track_id = track.id.clone(); @@ -1084,16 +1094,31 @@ impl QobuzSource { None }; - // 2. Register lazy entry + set cover_pk + seed metadata + // 2. Register lazy entry + set cover_pk + seed metadata. + // Si le performer est absent (réponse API incomplète), on passe None pour + // que le provider appelle get_track et récupère les métadonnées complètes. + if metadata.artist.is_none() { + tracing::warn!( + "register_tracks_lazy: no performer for track {} (id={}), will call provider", + track_title, track_id + ); + } + if cover_pk.is_none() && cover_image_url.is_some() { + tracing::warn!( + "register_tracks_lazy: cover download failed for track {} (id={}), will call provider for cover", + track_title, track_id + ); + } + let meta_hint = if metadata.artist.is_some() { Some(metadata) } else { None }; match source.inner.cache_manager - .cache_audio_lazy_with_provider(&lazy_pk, Some(metadata), cover_pk) + .cache_audio_lazy_with_provider(&lazy_pk, meta_hint, cover_pk) .await { Ok(pk) => { let _ = source.inner.cache_manager.set_audio_metadata( &pk, "qobuz_track_id", json!(track_id), ); - Some(pk) + Some((idx, pk)) } Err(e) => { tracing::warn!("Failed to register lazy track {}: {}", track_title, e); @@ -1103,12 +1128,15 @@ impl QobuzSource { } }).collect(); - tokio::task::JoinSet::from_iter(futs) + // Retrier par index original pour conserver l'ordre de la playlist Qobuz + let mut results: Vec<(usize, String)> = tokio::task::JoinSet::from_iter(futs) .join_all() .await .into_iter() .flatten() - .collect() + .collect(); + results.sort_unstable_by_key(|(i, _)| *i); + results.into_iter().map(|(_, pk)| pk).collect() } /// Cache les covers d'une liste d'artistes en parallèle. diff --git a/pmoradiofrance/src/metadata_cache.rs b/pmoradiofrance/src/metadata_cache.rs index 0da2f191..53299e4a 100644 --- a/pmoradiofrance/src/metadata_cache.rs +++ b/pmoradiofrance/src/metadata_cache.rs @@ -26,9 +26,6 @@ use tokio::sync::RwLock; #[cfg(feature = "cache")] use pmocovers::Cache as CoverCache; -#[cfg(feature = "cache")] -use pmocache::cache_trait::FileCache; - // ============================================================================ // CachedMetadata // ============================================================================ @@ -583,11 +580,7 @@ impl MetadataCache { } Err(discover_err) => { #[cfg(feature = "logging")] - tracing::error!( - "Rediscovery failed for '{}': {}", - slug, - discover_err - ); + tracing::error!("Rediscovery failed for '{}': {}", slug, discover_err); return Err(e); } } @@ -697,12 +690,7 @@ impl MetadataCache { match self.client.rediscover_station(slug).await { Ok((id, url)) => { #[cfg(feature = "logging")] - tracing::info!( - "Re-discovered station '{}': id={}, url={}", - slug, - id, - url - ); + tracing::info!("Re-discovered station '{}': id={}, url={}", slug, id, url); self.persist_station_mapping(); // Invalider le cache mémoire pour forcer un refresh des métadonnées let mut cache = self.cache.write().await; @@ -801,11 +789,18 @@ mod tests { assert!(base.is_expired()); // end_time dans le futur → non expiré - let not_expired = CachedMetadata { end_time: Some(now + 3600), ..base.clone() }; + let not_expired = CachedMetadata { + end_time: Some(now + 3600), + ..base.clone() + }; assert!(!not_expired.is_expired()); // Pas de end_time, fetched_at récent → non expiré (fallback TTL) - let no_end_fresh = CachedMetadata { end_time: None, fetched_at: now, ..base.clone() }; + let no_end_fresh = CachedMetadata { + end_time: None, + fetched_at: now, + ..base.clone() + }; assert!(!no_end_fresh.is_expired()); // Pas de end_time, fetched_at ancien → expiré diff --git a/pmosource/src/cache.rs b/pmosource/src/cache.rs index 57c50742..fed929ac 100755 --- a/pmosource/src/cache.rs +++ b/pmosource/src/cache.rs @@ -328,6 +328,7 @@ impl SourceCacheManager { None }; + let metadata_is_some = metadata.is_some(); let mut final_metadata = metadata; if final_metadata.is_none() { if let Some(data) = provider_data.as_ref() { @@ -352,10 +353,25 @@ impl SourceCacheManager { if let Some(meta) = final_metadata.as_ref() { self.seed_audio_metadata(lazy_pk, meta); + } else { + #[cfg(feature = "server")] + tracing::warn!( + "cache_audio_lazy_with_provider: no metadata to seed for {} \ + (meta_hint={}, provider_data={})", + lazy_pk, + metadata_is_some, + provider_data.as_ref().map(|d| d.metadata.is_some()).unwrap_or(false) + ); } if let Some(cover_pk) = final_cover_pk { let _ = self.set_audio_metadata(lazy_pk, "cover_pk", json!(cover_pk)); + } else { + #[cfg(feature = "server")] + tracing::debug!( + "cache_audio_lazy_with_provider: no cover_pk seeded for {}", + lazy_pk + ); } Ok(lazy_pk.to_string())