🚀 v0.3.32 — Add NoMedia handling & debug logging for Chromecast auto-advance

- Bump version to v0.3.32 (Cargo.toml, Cargo.lock &version.txt)
- Add detailed debug logging in ChromecastRenderer::playback_state()– tracks apps, media entries & player state
- Implement proper handling of PlaybackState::NoMedia to support auto advancing after track ends (Chromecast-specific behavior)
- Reorganize imports in musicrenderer.rs for clarity
- Minor cleanup: remove unused import, fix formatting
This commit is contained in:
2026-04-04 00:02:19 +02:00
parent 5e3fdd1841
commit 169e536011
5 changed files with 104 additions and 16 deletions

2
Cargo.lock generated
View File

@@ -4,7 +4,7 @@ version = 4
[[package]]
name = "PMOMusic"
version = "0.3.30"
version = "0.3.32"
dependencies = [
"axum 0.8.7",
"console-subscriber",

View File

@@ -1,6 +1,6 @@
[package]
name = "PMOMusic"
version = "0.3.30"
version = "0.3.32"
edition = "2024"
[dependencies]

View File

@@ -569,10 +569,21 @@ impl PlaybackStatus for ChromecastRenderer {
ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e))
})?;
tracing::debug!(
"Chromecast playback_state: {} apps running",
status.applications.len()
);
// If no app is running, return NoMedia
let app = match status.applications.first() {
Some(app) => app,
None => return Ok(PlaybackState::NoMedia),
Some(app) => {
tracing::debug!("Chromecast playback_state: app={}", app.display_name);
app
}
None => {
tracing::debug!("Chromecast playback_state: no apps running, returning NoMedia");
return Ok(PlaybackState::NoMedia);
}
};
// Connect to the app
@@ -591,10 +602,25 @@ impl PlaybackStatus for ChromecastRenderer {
ControlPointError::ChromecastError(format!("Failed to get media status: {}", e))
})?;
tracing::debug!(
"Chromecast playback_state: {} media entries",
media_status.entries.len()
);
// If no media entry, return NoMedia
let media_entry = match media_status.entries.first() {
Some(entry) => entry,
None => return Ok(PlaybackState::NoMedia),
Some(entry) => {
tracing::debug!(
"Chromecast playback_state: player_state={:?}, current_time={:?}",
entry.player_state,
entry.current_time
);
entry
}
None => {
tracing::debug!("Chromecast playback_state: no media entries, returning NoMedia");
return Ok(PlaybackState::NoMedia);
}
};
Ok(map_player_state(&media_entry.player_state))

View File

@@ -18,7 +18,6 @@ use crate::errors::ControlPointError;
use crate::events::RendererEventBus;
use crate::model::RendererEvent;
use crate::model::{PlaybackSource, PlaybackState, RendererInfo, RendererProtocol, TrackMetadata};
use crate::music_renderer::RendererFromMediaRendererInfo;
use crate::music_renderer::arylic_tcp::ArylicTcpRenderer;
use crate::music_renderer::capabilities::{
PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, QueueTransportControl, RendererBackend,
@@ -30,13 +29,12 @@ use crate::music_renderer::openhome_renderer::OpenHomeRenderer;
use crate::music_renderer::sleep_timer::SleepTimer;
use crate::music_renderer::upnp_renderer::UpnpRenderer;
use crate::music_renderer::watcher::{
WatchStrategy, WatchedState, extract_track_metadata, playback_position_equal,
playback_state_equal,
extract_track_metadata, playback_position_equal, playback_state_equal, WatchStrategy,
WatchedState,
};
use crate::music_renderer::RendererFromMediaRendererInfo;
use crate::online::DeviceConnectionState;
use crate::queue::{
EnqueueMode, MusicQueue, PlaybackItem, QueueBackend, QueueSnapshot,
};
use crate::queue::{EnqueueMode, MusicQueue, PlaybackItem, QueueBackend, QueueSnapshot};
use crate::{DeviceId, DeviceIdentity, DeviceOnline};
use tracing::warn;
@@ -639,10 +637,11 @@ impl MusicRenderer {
PlaybackState::Stopped => {
{
let s = self.state.lock().unwrap();
tracing::trace!(
tracing::debug!(
renderer = self.info.friendly_name(),
has_played = s.has_played_since_track_start,
playback_source = ?s.playback_source,
user_stop_requested = s.user_stop_requested,
"STOPPED detected — evaluating auto-advance"
);
}
@@ -705,6 +704,70 @@ impl MusicRenderer {
self.clear_has_played_flag();
}
}
PlaybackState::NoMedia => {
// Handle end of track (Chromecast returns NoMedia when track ends)
// This is equivalent to Stopped for auto-advance purposes
let s = self.state.lock().unwrap();
let playback_source = s.playback_source;
let has_played = s.has_played_since_track_start;
let user_stop = s.user_stop_requested;
drop(s);
tracing::debug!(
renderer = self.info.friendly_name(),
has_played = has_played,
playback_source = ?playback_source,
user_stop_requested = user_stop,
"NoMedia detected — evaluating auto-advance"
);
// Check if user requested stop (via Stop button in UI)
if self.check_and_clear_user_stop_requested() {
debug!(
renderer = self.info.friendly_name(),
"NoMedia after user request; not auto-advancing"
);
self.set_playback_source(PlaybackSource::None);
self.clear_has_played_flag();
} else if matches!(playback_source, PlaybackSource::FromQueue) {
// Auto-advance if we have seen a PLAYING state
if self.check_and_clear_has_played_flag() {
debug!(
renderer = self.info.friendly_name(),
"NoMedia after queue-driven playback; advancing to next track"
);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
self.play_next_from_queue()
}));
match result {
Ok(Ok(())) => {}
Ok(Err(err)) => {
error!(
renderer = self.info.friendly_name(),
error = %err,
"Auto-advance from NoMedia failed; clearing queue playback state"
);
self.set_playback_source(PlaybackSource::None);
}
Err(_panic) => {
error!(
renderer = self.info.friendly_name(),
"Auto-advance from NoMedia panicked; clearing queue playback state"
);
self.set_playback_source(PlaybackSource::None);
}
}
} else {
tracing::debug!(
renderer = self.info.friendly_name(),
"NoMedia but no PLAYING state seen yet; ignoring"
);
}
} else {
self.set_playback_source(PlaybackSource::None);
self.clear_has_played_flag();
}
}
PlaybackState::Playing => {
self.mark_external_if_idle();
// Mark that we have seen a PLAYING state - auto-advance is now allowed
@@ -1605,8 +1668,8 @@ pub(crate) fn build_didl_lite_metadata(
uri: &str,
protocol_info: &str,
) -> String {
use pmodidl::{DIDLLite, Item, Resource};
use pmodidl::ToXmlElement;
use pmodidl::{DIDLLite, Item, Resource};
// Construire l'Item DIDL avec toutes les métadonnées
let item = Item {
@@ -1842,7 +1905,6 @@ fn parse_rfc3339_to_system_time(s: &str) -> Option<SystemTime> {
Some(std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs as u64))
}
/// Transport control façade that dispatches to whichever backend can fulfill
/// the request, returning a standardized error if the backend lacks support.
impl TransportControl for MusicRendererBackend {

View File

@@ -1 +1 @@
0.3.30
0.3.32