diff --git a/PMOMusic/Cargo.toml b/PMOMusic/Cargo.toml index 703c428a..f80e2624 100644 --- a/PMOMusic/Cargo.toml +++ b/PMOMusic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "PMOMusic" -version = "0.3.39" +version = "0.3.40" edition = "2024" [dependencies] diff --git a/pmocontrol/src/media_server.rs b/pmocontrol/src/media_server.rs index 63b629bf..d813d2d3 100644 --- a/pmocontrol/src/media_server.rs +++ b/pmocontrol/src/media_server.rs @@ -2,8 +2,8 @@ use std::sync::{Arc, Mutex}; use std::time::SystemTime; use pmodidl::DIDLLite; -use pmoupnp::soap::SoapEnvelope; use pmoupnp::soap::error_codes; +use pmoupnp::soap::SoapEnvelope; use tracing::{debug, warn}; use xmltree::{Element, XMLNode}; @@ -11,8 +11,8 @@ use crate::errors::ControlPointError; use crate::model::TrackMetadata; use crate::online::{DeviceConnectionState, DeviceOnline}; use crate::queue::PlaybackItem; -use crate::soap_client::{SoapCallResult, invoke_upnp_action_with_timeout}; -use crate::{DEFAULT_HTTP_TIMEOUT, DeviceId, DeviceIdentity}; +use crate::soap_client::{invoke_upnp_action_with_timeout, SoapCallResult}; +use crate::{DeviceId, DeviceIdentity, DEFAULT_HTTP_TIMEOUT}; /// Snapshot of a media server discovered through UPnP SSDP. #[derive(Clone, Debug)] @@ -91,7 +91,9 @@ impl UpnpMediaServer { start: u32, count: u32, ) -> Result, ControlPointError> { - Ok(self.browse_with_flag_paged(object_id, browse_flag, start, count)?.entries) + Ok(self + .browse_with_flag_paged(object_id, browse_flag, start, count)? + .entries) } fn browse_with_flag_paged( @@ -109,17 +111,23 @@ impl UpnpMediaServer { ("Filter", "*".to_string()), ("StartingIndex", start_str), ("RequestedCount", count_str), - ("SortCriteria", String::new()), + // Tri par titre (alphabétique croissant) pour avoir un ordre déterministe + ("SortCriteria", "+dc:title".to_string()), ]; let response = self.invoke_content_directory("Browse", None, args)?; let envelope = response.envelope.ok_or_else(|| { - ControlPointError::MediaServerError("Missing SOAP envelope in Browse response".to_string()) + ControlPointError::MediaServerError( + "Missing SOAP envelope in Browse response".to_string(), + ) })?; let total_count = extract_total_matches(&envelope, "BrowseResponse"); let didl_xml = extract_result_payload(&envelope, "BrowseResponse")?; let entries = map_didl_entries(&didl_xml)?; - Ok(BrowsePage { entries, total_count }) + Ok(BrowsePage { + entries, + total_count, + }) } fn has_content_directory(&self) -> bool { @@ -398,7 +406,10 @@ pub trait MediaBrowser { ) -> Result { let entries = self.browse_children(object_id, start, count)?; let total_count = entries.len() as u32 + start; - Ok(BrowsePage { entries, total_count }) + Ok(BrowsePage { + entries, + total_count, + }) } fn browse_object(&self, object_id: &str) -> Result; fn search( diff --git a/pmomediaserver/src/content_handler.rs b/pmomediaserver/src/content_handler.rs index 80ebffec..21e71489 100644 --- a/pmomediaserver/src/content_handler.rs +++ b/pmomediaserver/src/content_handler.rs @@ -432,6 +432,12 @@ impl ContentHandler { items.len() ); + // Trier les containers par titre pour ordre déterministe (alphabétique, insensible à la casse) + // Les HashMap/HashSet utilisés pour la déduplication ne garantissent pas l'ordre, + // et les APIs externes peuvent renvoyer dans un ordre non-déterministe + // On NE trie PAS les items (pistes) car l'ordre original (numéro de piste) est important + containers.sort_by(|a, b| a.title.to_lowercase().cmp(&b.title.to_lowercase())); + // Calculer le total avant pagination let total = (containers.len() + items.len()) as u32; diff --git a/version.txt b/version.txt index a7c15395..a02af241 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.3.39 +0.3.40