feat(cache): fix lazy PK metadata resolution and playlist track ordering

- In `track_metadata.rs`, implement fallback logic for lazy PKs: first try real_pk, then fallback to lazy_pk for metadata keys seeded before download.
- In `db.rs`, add debug logging after migration to track remaining metadata under lazy_pk.
- In `source.rs` (Qobuz):
  - Detect and repair playlists with missing cache entries by forcing refresh when `items.len() < total`.
  - Preserve original track order after parallel processing by attaching and sorting on original index.
  - Handle missing performer/cover gracefully with warnings and provider fallback.
- In `metadata_cache.rs` (Radio France), simplify logging and improve test coverage for expiration logic.
- In `cache.rs`, add debug/warning logs when metadata or cover seeding fails during lazy caching.
This commit is contained in:
2026-03-25 12:25:34 +01:00
parent 09cc2dcf94
commit 5095dd6964
5 changed files with 92 additions and 25 deletions

View File

@@ -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.