Debug playlist lecture

This commit is contained in:
2025-12-28 15:27:48 +01:00
parent 2f77913caa
commit 02a44e9e75
8 changed files with 125 additions and 8 deletions

View File

@@ -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(())

View File

@@ -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"))

View File

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

View File

@@ -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(())

View File

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

View File

@@ -55,6 +55,10 @@ pub async fn playlist_events_sse(Query(params): Query<EventsQuery>) -> 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()))
}

View File

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

View File

@@ -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()
}