From 02a44e9e75d78efbe835b34d85d4d42de25ae10f Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sun, 28 Dec 2025 15:27:48 +0100 Subject: [PATCH] Debug playlist lecture --- pmocontrol/src/control_point.rs | 32 +++++++++++++++++- pmocontrol/src/music_renderer.rs | 56 +++++++++++++++++++++++++++++--- pmoparadise/src/source.rs | 8 +++++ pmoplaylist/src/handle/write.rs | 3 +- pmoplaylist/src/manager.rs | 15 +++++++++ pmoplaylist/src/sse.rs | 4 +++ pmoqobuz/src/api/catalog.rs | 7 ++++ pmoqobuz/src/models.rs | 8 ++++- 8 files changed, 125 insertions(+), 8 deletions(-) diff --git a/pmocontrol/src/control_point.rs b/pmocontrol/src/control_point.rs index 9f7cf150..9fe63297 100644 --- a/pmocontrol/src/control_point.rs +++ b/pmocontrol/src/control_point.rs @@ -1833,7 +1833,14 @@ impl ControlPoint { binding: Some(binding), }); - let mut auto_start_cb = |rid: &RendererId| self.start_queue_playback_if_idle(rid); + // For initial attach with auto_play, force playback start (don't check if idle) + let mut auto_start_cb = |rid: &RendererId| { + debug!( + renderer = rid.0.as_str(), + "Attach callback: forcing playback start (not checking if idle)" + ); + self.play_current_from_queue(rid) + }; let callback: Option<&mut dyn FnMut(&RendererId) -> anyhow::Result<()>> = if auto_play { Some(&mut auto_start_cb) } else { @@ -2683,6 +2690,12 @@ fn refresh_attached_queue_for( if auto_play { if let Some(callback) = after_refresh.as_deref_mut() { + debug!( + renderer = renderer_id.0.as_str(), + server = server_id.0.as_str(), + container = container_id.as_str(), + "Auto-play enabled: calling callback to start playback" + ); if let Err(err) = callback(renderer_id) { warn!( renderer = renderer_id.0.as_str(), @@ -2691,8 +2704,25 @@ fn refresh_attached_queue_for( error = %err, "Failed to auto-start playback after playlist refresh" ); + } else { + info!( + renderer = renderer_id.0.as_str(), + server = server_id.0.as_str(), + container = container_id.as_str(), + "Auto-play callback completed successfully" + ); } + } else { + debug!( + renderer = renderer_id.0.as_str(), + "Auto-play enabled but no callback provided" + ); } + } else { + debug!( + renderer = renderer_id.0.as_str(), + "Auto-play disabled, skipping playback start" + ); } Ok(()) diff --git a/pmocontrol/src/music_renderer.rs b/pmocontrol/src/music_renderer.rs index 315dada0..3b0ee05a 100644 --- a/pmocontrol/src/music_renderer.rs +++ b/pmocontrol/src/music_renderer.rs @@ -22,7 +22,7 @@ use crate::{ PlaybackPosition, PlaybackState, TransportControl, UpnpRenderer, VolumeControl, }; use anyhow::{Result, anyhow}; -use tracing::warn; +use tracing::{debug, info, warn}; /// Backend-agnostic façade exposing transport, volume, and status contracts. #[derive(Clone, Debug)] @@ -281,8 +281,18 @@ impl MusicRenderer { pub fn clear_for_playlist_attach(&self) -> Result<()> { match self { MusicRenderer::OpenHome(_) => { - // For OpenHome: clear the playlist on the renderer itself - self.openhome_playlist_clear() + // For OpenHome: clear the playlist (DeleteAll also stops playback automatically) + // then explicitly stop to ensure clean state + self.openhome_playlist_clear()?; + self.stop().or_else(|err| -> Result<()> { + // If stop fails (e.g., already stopped), that's fine + warn!( + renderer = self.id().0.as_str(), + error = %err, + "Stop failed after clearing OpenHome playlist (continuing anyway)" + ); + Ok(()) + }) } MusicRenderer::Upnp(_) | MusicRenderer::Chromecast(_) @@ -436,21 +446,57 @@ impl MusicRenderer { // Get the current OpenHome playlist snapshot let snapshot = self.fetch_openhome_playlist_snapshot()?; + debug!( + renderer = self.id().0.as_str(), + tracks_count = snapshot.tracks.len(), + current_id = ?snapshot.current_id, + current_index = ?snapshot.current_index, + "play_current_from_backend_queue: OpenHome snapshot fetched" + ); + if snapshot.tracks.is_empty() { return Err(anyhow!("OpenHome playlist is empty")); } // Find the track_id to play (prefer current_id, then current_index, then first) let target_track_id = if let Some(current_id) = snapshot.current_id { + debug!( + renderer = self.id().0.as_str(), + track_id = current_id, + "Using current_id for playback" + ); Some(current_id) } else if let Some(current_idx) = snapshot.current_index { - snapshot.tracks.get(current_idx).map(|track| track.id) + let track_id = snapshot.tracks.get(current_idx).map(|track| track.id); + debug!( + renderer = self.id().0.as_str(), + current_idx, + track_id = ?track_id, + "Using current_index for playback" + ); + track_id } else { - snapshot.tracks.first().map(|track| track.id) + let track_id = snapshot.tracks.first().map(|track| track.id); + debug!( + renderer = self.id().0.as_str(), + track_id = ?track_id, + "Using first track for playback" + ); + track_id }; if let Some(track_id) = target_track_id { + info!( + renderer = self.id().0.as_str(), + track_id, + "Calling openhome_playlist_play_id to start playback" + ); self.openhome_playlist_play_id(track_id)?; + info!( + renderer = self.id().0.as_str(), + track_id, + "Successfully called openhome_playlist_play_id" + ); Ok(()) } else { Err(anyhow!("No track to play in OpenHome playlist")) diff --git a/pmoparadise/src/source.rs b/pmoparadise/src/source.rs index 459998dd..7de3c492 100644 --- a/pmoparadise/src/source.rs +++ b/pmoparadise/src/source.rs @@ -135,6 +135,14 @@ impl RadioParadiseSource { let token = mgr.register_callback(move |event| { let pid = pid_clone.clone(); if event.playlist_id == pid { + // Ignorer PkUpdated - pas de notification UPnP (évite le reload côté control point) + if matches!(event.kind, pmoplaylist::PlaylistEventKind::PkUpdated { .. }) { + tracing::debug!( + "PK swap in playlist {} - no UPnP notification (prevents playback restart)", + event.playlist_id + ); + return; + } // On ne réagit qu'aux mises à jour structurelles (ajout/suppression) if !matches!(event.kind, pmoplaylist::PlaylistEventKind::Updated) { return; diff --git a/pmoplaylist/src/handle/write.rs b/pmoplaylist/src/handle/write.rs index f49997a5..de26b8ad 100644 --- a/pmoplaylist/src/handle/write.rs +++ b/pmoplaylist/src/handle/write.rs @@ -527,7 +527,8 @@ impl WriteHandle { manager .rebuild_track_index(&self.playlist.id, &snapshot) .await; - manager.notify_playlist_changed(&self.playlist.id); + // PK swap uniquement - pas de notification UPnP pour éviter le reload + manager.notify_playlist_pk_updated(&self.playlist.id, old_pk, new_pk); } Ok(()) diff --git a/pmoplaylist/src/manager.rs b/pmoplaylist/src/manager.rs index 87168119..64fdfb3b 100644 --- a/pmoplaylist/src/manager.rs +++ b/pmoplaylist/src/manager.rs @@ -48,6 +48,9 @@ pub struct PlaylistEvent { pub enum PlaylistEventKind { /// La playlist a été modifiée (ajout/suppression/changement de config). Updated, + /// Cache PK commuté (lazy→real) - pas de changement structurel. + /// N'émet PAS de ContainersUpdated UPnP pour éviter le reload. + PkUpdated { old_pk: String, new_pk: String }, /// Un morceau référencé par la playlist a été servi par le cache audio. TrackPlayed { cache_pk: String, qualifier: String }, } @@ -270,6 +273,18 @@ impl PlaylistManager { self.notify_playlist_event(id, PlaylistEventKind::Updated); } + /// Notifie que des PK ont été swappés (lazy→real). + /// N'émet PAS de notification UPnP ContainersUpdated pour éviter le reload. + pub(crate) fn notify_playlist_pk_updated(&self, id: &str, old_pk: &str, new_pk: &str) { + self.notify_playlist_event( + id, + PlaylistEventKind::PkUpdated { + old_pk: old_pk.to_string(), + new_pk: new_pk.to_string(), + }, + ); + } + /// Notifie les callbacks qu'un morceau a été joué pour une playlist donnée. pub(crate) fn notify_playlist_track_played( &self, diff --git a/pmoplaylist/src/sse.rs b/pmoplaylist/src/sse.rs index 1353a17f..bb07d258 100644 --- a/pmoplaylist/src/sse.rs +++ b/pmoplaylist/src/sse.rs @@ -55,6 +55,10 @@ pub async fn playlist_events_sse(Query(params): Query) -> impl Into let (kind, cache_pk, qualifier) = match &envelope.event.kind { PlaylistEventKind::Updated => ("updated", None, None), + PlaylistEventKind::PkUpdated { old_pk: _, new_pk: _ } => { + // PK swap silencieux - envoyer quand même l'événement SSE pour debug/monitoring + ("pk_updated", None, None) + } PlaylistEventKind::TrackPlayed { cache_pk, qualifier } => { ("track_played", Some(cache_pk.as_str()), Some(qualifier.as_str())) } diff --git a/pmoqobuz/src/api/catalog.rs b/pmoqobuz/src/api/catalog.rs index aacbecf2..99dd8b9e 100644 --- a/pmoqobuz/src/api/catalog.rs +++ b/pmoqobuz/src/api/catalog.rs @@ -479,6 +479,13 @@ impl QobuzApi { // Fonctions de parsing publiques (utilisées aussi par le module user) pub(crate) fn parse_album(response: AlbumResponse) -> Album { + // Log pour débugger les valeurs audio + if let Some(rate) = response.maximum_sampling_rate { + debug!("Album {} - maximum_sampling_rate: {} Hz", response.id, rate); + } else { + debug!("Album {} - maximum_sampling_rate: None", response.id); + } + Album { id: response.id, title: response.title, diff --git a/pmoqobuz/src/models.rs b/pmoqobuz/src/models.rs index 08f082b5..5de0baf1 100644 --- a/pmoqobuz/src/models.rs +++ b/pmoqobuz/src/models.rs @@ -269,7 +269,13 @@ impl Album { /// Retourne un titre formaté avec les informations audio si disponibles pub fn formatted_title(&self) -> String { if let (Some(rate), Some(depth)) = (self.maximum_sampling_rate, self.maximum_bit_depth) { - format!("{} ({:.0}/{} bit)", self.title, rate / 1000.0, depth) + // Convertir Hz en kHz, en gérant les valeurs qui pourraient déjà être en kHz + let rate_khz = if rate > 1000.0 { + rate / 1000.0 + } else { + rate + }; + format!("{} ({:.1} kHz / {} bits)", self.title, rate_khz, depth) } else { self.title.clone() }