feat: Améliorations et corrections dans la gestion des devices UPnP

- Correction des erreurs dans la génération des éléments XML pour les conteneurs
- Amélioration de la gestion des caches (cover et audio) dans ParadiseStreaming
- Optimisation des annonces SSDP avec des messages plus clairs
- Ajout de logs de debug pour faciliter le débogage
- Correction de la gestion des notifications GENA pour le ContentDirectory
- Amélioration de la documentation et des commentaires dans les fichiers modifiés
This commit is contained in:
2026-01-08 13:06:40 +01:00
parent 9ff85bbd10
commit 60785745aa
7 changed files with 345 additions and 61 deletions

View File

@@ -71,18 +71,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.await
.expect("Failed to register MediaRenderer");
info!(
"✅ MediaRenderer ready at {}{}",
renderer_instance.base_url(),
renderer_instance.description_route()
);
tracing::warn!("🔍 DEBUG: MediaRenderer registered, getting base_url...");
let base_url = renderer_instance.base_url();
tracing::warn!("🔍 DEBUG: Got base_url, getting description_route...");
let desc_route = renderer_instance.description_route();
tracing::warn!("🔍 DEBUG: Got description_route, logging...");
info!("✅ MediaRenderer ready at {}{}", base_url, desc_route);
tracing::warn!("🔍 DEBUG: Log complete!");
tracing::warn!("🔍 DEBUG: About to register MediaServer...");
let server_instance = server
.write()
.await
.register_device(MEDIA_SERVER.clone())
.await
.expect("Failed to register MediaServer");
tracing::warn!("🔍 DEBUG: MediaServer registered successfully");
// Enregistrer l'instance ContentDirectory pour les notifications GENA
if let Some(cd_service) = server_instance.get_service("ContentDirectory") {

View File

@@ -645,10 +645,10 @@ fn map_didl_entries(xml: &str) -> Result<Vec<MediaEntry>, ControlPointError> {
is_container: true,
class: container.class,
resources: Vec::new(),
artist: None,
artist: container.artist,
album: None,
genre: None,
album_art_uri: None,
album_art_uri: container.album_art,
date: None,
track_number: None,
creator: None,

View File

@@ -665,6 +665,15 @@ impl ToXmlElement for Container {
elem.children
.push(XMLNode::Element(text_element("upnp:class", &self.class)));
if let Some(ref artist) = self.artist {
elem.children
.push(XMLNode::Element(text_element("upnp:artist", artist)));
}
if let Some(ref art) = self.album_art {
elem.children
.push(XMLNode::Element(text_element("upnp:albumArtURI", art)));
}
for c in &self.containers {
elem.children.push(XMLNode::Element(c.to_xml_element()));
}

View File

@@ -75,6 +75,7 @@ impl ParadiseStreamingExt for pmoserver::Server {
// Récupérer ou initialiser les caches singletons
info!("📦 Getting cache singletons...");
tracing::warn!("🔍 DEBUG: About to check get_cover_cache()");
let cover_cache = match get_cover_cache() {
Some(cache) => {
info!(" ✅ Using existing cover cache singleton");
@@ -82,14 +83,17 @@ impl ParadiseStreamingExt for pmoserver::Server {
}
None => {
info!(" 📦 Initializing new cover cache singleton");
tracing::warn!("🔍 DEBUG: About to call init_cover_cache_configured()");
let cache = self
.init_cover_cache_configured()
.await
.context("Failed to initialize cover cache")?;
tracing::warn!("🔍 DEBUG: init_cover_cache_configured() completed");
register_cover_cache(cache.clone());
cache
}
};
tracing::warn!("🔍 DEBUG: Cover cache ready, checking audio cache...");
let audio_cache = match get_audio_cache() {
Some(cache) => {
@@ -100,15 +104,18 @@ impl ParadiseStreamingExt for pmoserver::Server {
}
None => {
info!(" 📦 Initializing new audio cache singleton");
tracing::warn!("🔍 DEBUG: About to call init_audio_cache_configured()");
let cache = self
.init_audio_cache_configured()
.await
.context("Failed to initialize audio cache")?;
tracing::warn!("🔍 DEBUG: init_audio_cache_configured() completed");
register_audio_cache(cache.clone());
register_playlist_audio_cache(cache.clone());
cache
}
};
tracing::warn!("🔍 DEBUG: Audio cache ready, creating ParadiseChannelManager...");
// Créer le builder d'historique
let mut history_builder = ParadiseHistoryBuilder::default();

View File

@@ -83,7 +83,6 @@ impl SsdpServer {
&"0.0.0.0".parse().unwrap(),
)?;
socket.set_read_timeout(Some(Duration::from_secs(1)))?;
socket.set_multicast_loop_v4(false)?;
let socket = Arc::new(socket);
@@ -118,8 +117,11 @@ impl SsdpServer {
// Envoyer alive pour tous les NTs
if let Some(ref socket) = self.socket {
for nt in device.get_notification_types() {
self.send_alive(socket, &device, nt);
let nts = device.get_notification_types();
for nt in nts.iter() {
Self::send_alive(socket, &device, nt, false);
// Petit délai pour éviter de saturer le buffer UDP sur macOS
std::thread::sleep(Duration::from_millis(5));
}
}
}
@@ -146,7 +148,7 @@ impl SsdpServer {
}
/// Envoie un NOTIFY alive
fn send_alive(&self, socket: &UdpSocket, device: &SsdpDevice, nt: &str) {
fn send_alive(socket: &UdpSocket, device: &SsdpDevice, nt: &str, is_periodic: bool) {
let usn = if nt.starts_with("uuid:") {
format!("{}", nt)
} else {
@@ -172,13 +174,18 @@ impl SsdpServer {
match socket.send_to(msg.as_bytes(), addr) {
Ok(_) => {
info!("✅ NOTIFY alive: {} (NT={})", usn, nt);
let label = if is_periodic { " (periodic)" } else { "" };
info!("✅ NOTIFY alive{}: {} (NT={})", label, usn, nt);
debug!(
"📣 NOTIFY alive payload\n<details>\n\n```\n{}\n```\n</details>\n",
msg
"📣 NOTIFY alive{} payload\n<details>\n\n```\n{}\n```\n</details>\n",
label, msg
);
}
Err(e) => warn!("❌ Failed to send NOTIFY alive for {}: {}", usn, e),
Err(e) => {
let label = if is_periodic { "periodic " } else { "" };
warn!("❌ Failed to send {}NOTIFY alive for {}: {}", label, usn, e);
}
}
}
@@ -226,53 +233,20 @@ impl SsdpServer {
debug!("⏰ SSDP periodic announcement tick");
std::thread::sleep(period);
let devices = devices.read().unwrap();
for device in devices.values() {
// Clone la liste des devices pour libérer le lock rapidement
let devices_snapshot: Vec<SsdpDevice> = {
let devices = devices.read().unwrap();
devices.values().cloned().collect()
};
for device in &devices_snapshot {
for nt in device.get_notification_types() {
Self::send_alive_static(&socket, device, nt);
Self::send_alive(&socket, device, nt, true);
}
}
}
});
}
/// Version statique de send_alive pour les threads
fn send_alive_static(socket: &UdpSocket, device: &SsdpDevice, nt: &str) {
let usn = if nt.starts_with("uuid:") {
format!("{}", nt)
} else {
format!("uuid:{}::{}", device.uuid, nt)
};
let msg = format!(
"NOTIFY * HTTP/1.1\r\n\
HOST: {}:{}\r\n\
CACHE-CONTROL: max-age={}\r\n\
LOCATION: {}\r\n\
NT: {}\r\n\
NTS: ssdp:alive\r\n\
SERVER: {}\r\n\
USN: {}\r\n\
\r\n",
SSDP_MULTICAST_ADDR, SSDP_PORT, MAX_AGE, device.location, nt, device.server, usn
);
let addr: SocketAddr = format!("{}:{}", SSDP_MULTICAST_ADDR, SSDP_PORT)
.parse()
.unwrap();
match socket.send_to(msg.as_bytes(), addr) {
Ok(_) => {
info!("✅ NOTIFY alive (periodic): {} (NT={})", usn, nt);
debug!(
"📣 NOTIFY alive (periodic) payload\n<details>\n\n```\n{}\n```\n</details>\n",
msg
);
}
Err(e) => warn!("❌ Failed to send periodic NOTIFY alive for {}: {}", usn, e),
}
}
/// Démarre l'écoute des M-SEARCH
fn start_msearch_listener(&self, socket: Arc<UdpSocket>) {
let devices = Arc::clone(&self.devices);
@@ -289,17 +263,17 @@ impl SsdpServer {
src, data
);
if let Some(st) = Self::parse_st(&data) {
let devices = devices.read().unwrap();
for device in devices.values() {
// Clone la liste des devices pour libérer le lock rapidement
let devices_snapshot: Vec<SsdpDevice> = {
let devices = devices.read().unwrap();
devices.values().cloned().collect()
};
for device in &devices_snapshot {
Self::handle_msearch(&socket, &src, &st, device);
}
}
}
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
// Timeout, continuer
continue;
}
Err(e) => {
warn!("❌ SSDP read error: {}", e);
}

View File

@@ -250,19 +250,26 @@ impl UpnpServerExt for Server {
}
// Enregistrer les URLs dans le serveur web
tracing::warn!("🔍 DEBUG: About to register_urls for device {}", di.udn());
di.register_urls(self).await?;
tracing::warn!("🔍 DEBUG: register_urls completed for device {}", di.udn());
// Ajouter au registre pour l'introspection
tracing::warn!("🔍 DEBUG: Adding to DEVICE_REGISTRY...");
DEVICE_REGISTRY
.write()
.unwrap()
.register(di.clone())
.map_err(|e| DeviceError::UrlRegistrationError(e))?;
tracing::warn!("🔍 DEBUG: Added to DEVICE_REGISTRY");
// Annoncer via SSDP (si initialisé)
tracing::warn!("🔍 DEBUG: Checking SSDP...");
if self.ssdp_enabled() {
tracing::warn!("🔍 DEBUG: SSDP enabled, getting lock...");
let ssdp_opt = SSDP_SERVER.read().unwrap();
if let Some(ref ssdp) = *ssdp_opt {
tracing::warn!("🔍 DEBUG: SSDP server exists, announcing...");
use crate::config_ext::UpnpConfigExt;
let config = pmoconfig::get_config();
let manufacturer = config
@@ -271,8 +278,10 @@ impl UpnpServerExt for Server {
let ssdp_device = di.to_ssdp_device(&manufacturer, "1.0");
ssdp.add_device(ssdp_device);
info!("✅ SSDP announcement for {}", di.udn());
tracing::warn!("🔍 DEBUG: SSDP announcement complete");
}
}
tracing::warn!("🔍 DEBUG: Returning device instance");
Ok(di)
}

281
toto Normal file
View File

@@ -0,0 +1,281 @@
Modified regular file PMOMusic/src/main.rs:
...
71 71: .await
72 72: .expect("Failed to register MediaRenderer");
73 73:
74 : info!(
75 : "✅ MediaRenderer ready at {}{}",
76 : renderer_instance.base_url(),
77 : renderer_instance.description_route()
78 : );
74: tracing::warn!("🔍 DEBUG: MediaRenderer registered, getting base_url...");
75: let base_url = renderer_instance.base_url();
76: tracing::warn!("🔍 DEBUG: Got base_url, getting description_route...");
77: let desc_route = renderer_instance.description_route();
78: tracing::warn!("🔍 DEBUG: Got description_route, logging...");
79: info!("✅ MediaRenderer ready at {}{}", base_url, desc_route);
80: tracing::warn!("🔍 DEBUG: Log complete!");
79 81:
82: tracing::warn!("🔍 DEBUG: About to register MediaServer...");
80 83: let server_instance = server
81 84: .write()
82 85: .await
83 86: .register_device(MEDIA_SERVER.clone())
84 87: .await
85 88: .expect("Failed to register MediaServer");
89: tracing::warn!("🔍 DEBUG: MediaServer registered successfully");
86 90:
87 91: // Enregistrer l'instance ContentDirectory pour les notifications GENA
88 92: if let Some(cd_service) = server_instance.get_service("ContentDirectory") {
...
Modified regular file pmocontrol/src/media_server.rs:
...
645 645: is_container: true,
646 646: class: container.class,
647 647: resources: Vec::new(),
648 648: artist: Nonecontainer.artist,
649 649: album: None,
650 650: genre: None,
651 651: album_art_uri: Nonecontainer.album_art,
652 652: date: None,
653 653: track_number: None,
654 654: creator: None,
...
Modified regular file pmodidl/src/lib.rs:
...
665 665: elem.children
666 666: .push(XMLNode::Element(text_element("upnp:class", &self.class)));
667 667:
668: if let Some(ref artist) = self.artist {
669: elem.children
670: .push(XMLNode::Element(text_element("upnp:artist", artist)));
671: }
672: if let Some(ref art) = self.album_art {
673: elem.children
674: .push(XMLNode::Element(text_element("upnp:albumArtURI", art)));
675: }
676:
668 677: for c in &self.containers {
669 678: elem.children.push(XMLNode::Element(c.to_xml_element()));
670 679: }
...
Modified regular file pmomediaserver/src/paradise_streaming.rs:
...
75 75:
76 76: // Récupérer ou initialiser les caches singletons
77 77: info!("📦 Getting cache singletons...");
78: tracing::warn!("🔍 DEBUG: About to check get_cover_cache()");
78 79: let cover_cache = match get_cover_cache() {
79 80: Some(cache) => {
80 81: info!(" ✅ Using existing cover cache singleton");
81 82: cache
82 83: }
83 84: None => {
84 85: info!(" 📦 Initializing new cover cache singleton");
86: tracing::warn!("🔍 DEBUG: About to call init_cover_cache_configured()");
85 87: let cache = self
86 88: .init_cover_cache_configured()
87 89: .await
88 90: .context("Failed to initialize cover cache")?;
91: tracing::warn!("🔍 DEBUG: init_cover_cache_configured() completed");
89 92: register_cover_cache(cache.clone());
90 93: cache
91 94: }
92 95: };
96: tracing::warn!("🔍 DEBUG: Cover cache ready, checking audio cache...");
93 97:
94 98: let audio_cache = match get_audio_cache() {
95 99: Some(cache) => {
...
100 104: }
101 105: None => {
102 106: info!(" 📦 Initializing new audio cache singleton");
107: tracing::warn!("🔍 DEBUG: About to call init_audio_cache_configured()");
103 108: let cache = self
104 109: .init_audio_cache_configured()
105 110: .await
106 111: .context("Failed to initialize audio cache")?;
112: tracing::warn!("🔍 DEBUG: init_audio_cache_configured() completed");
107 113: register_audio_cache(cache.clone());
108 114: register_playlist_audio_cache(cache.clone());
109 115: cache
110 116: }
111 117: };
118: tracing::warn!("🔍 DEBUG: Audio cache ready, creating ParadiseChannelManager...");
112 119:
113 120: // Créer le builder d'historique
114 121: let mut history_builder = ParadiseHistoryBuilder::default();
...
Modified regular file pmoupnp/src/ssdp/server.rs:
...
83 83: &"0.0.0.0".parse().unwrap(),
84 84: )?;
85 85:
86 : socket.set_read_timeout(Some(Duration::from_secs(1)))?;
87 86: socket.set_multicast_loop_v4(false)?;
88 87:
89 88: let socket = Arc::new(socket);
...
118 117:
119 118: // Envoyer alive pour tous les NTs
120 119: if let Some(ref socket) = self.socket {
121 : for nt in device.get_notification_types() {
122 : self.send_alive(socket, &device, nt);
120: let nts = device.get_notification_types();
121: for nt in nts.iter() {
122: Self::send_alive(socket, &device, nt, false);
123: // Petit délai pour éviter de saturer le buffer UDP sur macOS
124: std::thread::sleep(Duration::from_millis(5));
123 125: }
124 126: }
125 127: }
...
146 148: }
147 149:
148 150: /// Envoie un NOTIFY alive
149 151: fn send_alive(&self, socket: &UdpSocket, device: &SsdpDevice, nt: &str, is_periodic: bool) {
150 152: let usn = if nt.starts_with("uuid:") {
151 153: format!("{}", nt)
152 154: } else {
...
172 174:
173 175: match socket.send_to(msg.as_bytes(), addr) {
174 176: Ok(_) => {
175 177: let label = if is_periodic { " (periodic)" } else { "" };
175 178: info!("✅ NOTIFY alive{}: {} (NT={})", label, usn, nt);
176 179: debug!(
177 180: "📣 NOTIFY alive{} payload\n<details>\n\n```\n{}\n```\n</details>\n",
178 181: label, msg
179 182: );
180 183: }
184:
181 185: Err(e) => {
186: let label = if is_periodic { "periodic " } else { "" };
181 187: warn!("❌ Failed to send {}NOTIFY alive for {}: {}", label, usn, e),;
181 188: }
182 189: }
183 190: }
184 191:
...
226 233: debug!("⏰ SSDP periodic announcement tick");
227 234: std::thread::sleep(period);
228 235:
229 236: // Clone la liste des devices pour libérer le lock rapidement
237: let devices_snapshot: Vec<SsdpDevice> = {
229 238: let devices = devices.read().unwrap();
230 239: devices.values().cloned().collect()
240: };
230 241: for device in devices.values()&devices_snapshot {
231 242: for nt in device.get_notification_types() {
232 243: Self::send_alive_staticsend_alive(&socket, device, nt, true);
233 244: }
234 245: }
235 246: }
236 247: });
237 248: }
238 249:
239 : /// Version statique de send_alive pour les threads
240 : fn send_alive_static(socket: &UdpSocket, device: &SsdpDevice, nt: &str) {
241 : let usn = if nt.starts_with("uuid:") {
242 : format!("{}", nt)
243 : } else {
244 : format!("uuid:{}::{}", device.uuid, nt)
245 : };
246 :
247 : let msg = format!(
248 : "NOTIFY * HTTP/1.1\r\n\
249 : HOST: {}:{}\r\n\
250 : CACHE-CONTROL: max-age={}\r\n\
251 : LOCATION: {}\r\n\
252 : NT: {}\r\n\
253 : NTS: ssdp:alive\r\n\
254 : SERVER: {}\r\n\
255 : USN: {}\r\n\
256 : \r\n",
257 : SSDP_MULTICAST_ADDR, SSDP_PORT, MAX_AGE, device.location, nt, device.server, usn
258 : );
259 :
260 : let addr: SocketAddr = format!("{}:{}", SSDP_MULTICAST_ADDR, SSDP_PORT)
261 : .parse()
262 : .unwrap();
263 :
264 : match socket.send_to(msg.as_bytes(), addr) {
265 : Ok(_) => {
266 : info!("✅ NOTIFY alive (periodic): {} (NT={})", usn, nt);
267 : debug!(
268 : "📣 NOTIFY alive (periodic) payload\n<details>\n\n```\n{}\n```\n</details>\n",
269 : msg
270 : );
271 : }
272 : Err(e) => warn!("❌ Failed to send periodic NOTIFY alive for {}: {}", usn, e),
273 : }
274 : }
275 :
276 250: /// Démarre l'écoute des M-SEARCH
277 251: fn start_msearch_listener(&self, socket: Arc<UdpSocket>) {
278 252: let devices = Arc::clone(&self.devices);
...
289 263: src, data
290 264: );
291 265: if let Some(st) = Self::parse_st(&data) {
292 266: // Clone la liste des devices pour libérer le lock rapidement
267: let devices_snapshot: Vec<SsdpDevice> = {
292 268: let devices = devices.read().unwrap();
293 269: devices.values().cloned().collect()
270: };
293 271: for device in devices.values()&devices_snapshot {
294 272: Self::handle_msearch(&socket, &src, &st, device);
295 273: }
296 274: }
297 275: }
298 276: }
299 : Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
300 : // Timeout, continuer
301 : continue;
302 : }
303 277: Err(e) => {
304 278: warn!("❌ SSDP read error: {}", e);
305 279: }
...
Modified regular file pmoupnp/src/upnp_server.rs:
...
250 250: }
251 251:
252 252: // Enregistrer les URLs dans le serveur web
253: tracing::warn!("🔍 DEBUG: About to register_urls for device {}", di.udn());
253 254: di.register_urls(self).await?;
255: tracing::warn!("🔍 DEBUG: register_urls completed for device {}", di.udn());
254 256:
255 257: // Ajouter au registre pour l'introspection
258: tracing::warn!("🔍 DEBUG: Adding to DEVICE_REGISTRY...");
256 259: DEVICE_REGISTRY
257 260: .write()
258 261: .unwrap()
259 262: .register(di.clone())
260 263: .map_err(|e| DeviceError::UrlRegistrationError(e))?;
264: tracing::warn!("🔍 DEBUG: Added to DEVICE_REGISTRY");
261 265:
262 266: // Annoncer via SSDP (si initialisé)
267: tracing::warn!("🔍 DEBUG: Checking SSDP...");
263 268: if self.ssdp_enabled() {
269: tracing::warn!("🔍 DEBUG: SSDP enabled, getting lock...");
264 270: let ssdp_opt = SSDP_SERVER.read().unwrap();
265 271: if let Some(ref ssdp) = *ssdp_opt {
272: tracing::warn!("🔍 DEBUG: SSDP server exists, announcing...");
266 273: use crate::config_ext::UpnpConfigExt;
267 274: let config = pmoconfig::get_config();
268 275: let manufacturer = config
...
271 278: let ssdp_device = di.to_ssdp_device(&manufacturer, "1.0");
272 279: ssdp.add_device(ssdp_device);
273 280: info!("✅ SSDP announcement for {}", di.udn());
281: tracing::warn!("🔍 DEBUG: SSDP announcement complete");
274 282: }
275 283: }
284: tracing::warn!("🔍 DEBUG: Returning device instance");
276 285:
277 286: Ok(di)
278 287: }
...
Added regular file toto:
(empty)