Je ne sais pas trop

This commit is contained in:
2025-10-26 23:49:20 +01:00
parent 078d6cb5f8
commit ea6dd03ac0
15 changed files with 66 additions and 24 deletions

BIN
.DS_Store vendored

Binary file not shown.

5
.gitignore vendored
View File

@@ -8,6 +8,8 @@
**/*.o
**/*.o.d
**/*.a
**/*.aif
**/*.aiff
xxx
/dcai/
**/.pmomusic.yml
@@ -15,7 +17,7 @@ xxx
**/.pmomusic_audio/**
/.pmomusic
.DS_Store
/target/
target
/.pmomusic_covers
/.pmomusic_audio/**
C/src/soxr-0.1.3/Release/tests
@@ -28,3 +30,4 @@ all.txt
pmo_src.txt
upmpdcli/
/*.xml
test_upnp

View File

@@ -125,6 +125,9 @@ pub struct Container {
#[serde(rename = "@childCount", skip_serializing_if = "Option::is_none")]
pub child_count: Option<String>,
#[serde(rename = "@searchable", skip_serializing_if = "Option::is_none")]
pub searchable: Option<String>,
#[serde(rename = "dc:title", alias = "title")]
pub title: String,

View File

@@ -28,7 +28,9 @@ fn to_didl_lite(containers: &[Container], items: &[pmodidl::Item]) -> Result<Str
items: items.to_vec(),
};
quick_xml::se::to_string(&didl).map_err(|e| format!("Failed to serialize DIDL-Lite: {}", e))
let body =
quick_xml::se::to_string(&didl).map_err(|e| format!("Failed to serialize DIDL-Lite: {}", e))?;
Ok(format!("<?xml version=\"1.0\" encoding=\"UTF-8\"?>{}", body))
}
/// Handler pour le service ContentDirectory
@@ -97,8 +99,8 @@ impl ContentHandler {
if object_id == "0" {
// Retourner le container racine
let root = self.build_root_container().await;
let didl = to_didl_lite(&[root], &[])?;
Ok((didl, 1, 1, 0))
let didl = to_didl_lite(&[root], &[])?;
Ok((didl, 1, 1, 1))
} else {
// Essayer de trouver l'objet dans les sources
// Vérifier si c'est un container racine d'une source
@@ -219,6 +221,8 @@ impl ContentHandler {
.root_container()
.await
.map_err(|e| format!("Failed to get root container: {}", e))?;
let mut container = container;
container.searchable = container.searchable.or_else(|| Some("1".to_string()));
containers.push(container);
}
@@ -236,7 +240,14 @@ impl ContentHandler {
let returned = paginated.len();
let didl = to_didl_lite(&paginated, &[])?;
Ok((didl, returned as u32, total as u32, 0))
// Aggregate update IDs across all sources
let mut combined_id = 0u32;
for source in list_all_sources().await {
combined_id = combined_id.wrapping_add(source.update_id().await);
}
let update_id = combined_id.max(1);
Ok((didl, returned as u32, total as u32, update_id))
}
/// Browse le container racine d'une source spécifique
@@ -317,6 +328,7 @@ impl ContentHandler {
parent_id: "-1".to_string(),
restricted: Some("1".to_string()),
child_count: Some(child_count.to_string()),
searchable: Some("1".to_string()),
title: "PMOMusic".to_string(),
class: "object.container".to_string(),
containers: vec![],
@@ -367,7 +379,19 @@ impl ContentHandler {
let total = (all_containers.len() + all_items.len()) as u32;
let didl = to_didl_lite(&all_containers, &all_items)?;
Ok((didl, total, total, 0))
// Compute a global update ID from active sources, ensure it starts at 1
let update_id = if total > 0 {
let sources = list_all_sources().await;
let mut combined_id = 0u32;
for source in sources {
combined_id = combined_id.wrapping_add(source.update_id().await);
}
combined_id.max(1)
} else {
1
};
Ok((didl, total, total, update_id))
}
/// Retourne les capacités de recherche
@@ -392,7 +416,7 @@ impl ContentHandler {
combined_id = combined_id.wrapping_add(source.update_id().await);
}
combined_id
combined_id.max(1)
}
}
@@ -419,9 +443,10 @@ mod tests {
let result = handler.browse("0", "BrowseDirectChildren", 0, 0).await;
assert!(result.is_ok());
let (didl, returned, total, _) = result.unwrap();
let (didl, returned, total, update_id) = result.unwrap();
assert_eq!(returned, 0);
assert_eq!(total, 0);
assert_eq!(update_id, 1);
assert!(didl.contains("DIDL-Lite"));
}
@@ -429,6 +454,6 @@ mod tests {
async fn test_get_system_update_id() {
let handler = ContentHandler::new();
let update_id = handler.get_system_update_id().await;
assert_eq!(update_id, 0); // No sources registered
assert_eq!(update_id, 1); // No sources registered -> minimum 1
}
}

View File

@@ -249,6 +249,7 @@ mod tests {
parent_id: "0".to_string(),
restricted: Some("1".to_string()),
child_count: Some("0".to_string()),
searchable: Some("1".to_string()),
title: self.name.clone(),
class: "object.container".to_string(),
containers: vec![],

View File

@@ -245,6 +245,7 @@ impl RadioParadiseSource {
parent_id: "0".to_string(),
restricted: Some("1".to_string()),
child_count: Some(ALL_CHANNELS.len().to_string()),
searchable: Some("1".to_string()),
title: "Radio Paradise".to_string(),
class: "object.container".to_string(),
containers: vec![],
@@ -262,6 +263,7 @@ impl RadioParadiseSource {
parent_id: "radio-paradise".to_string(),
restricted: Some("1".to_string()),
child_count: Some(len.to_string()),
searchable: Some("1".to_string()),
title: descriptor.display_name.to_string(),
class: "object.container.playlistContainer".to_string(),
containers: vec![],

View File

@@ -504,6 +504,7 @@ impl FifoPlaylist {
parent_id: parent_id.into(),
restricted: Some("1".to_string()),
child_count: Some(inner.queue.len().to_string()),
searchable: Some("1".to_string()),
title: inner.title.clone(),
class: "object.container.playlistContainer".to_string(),
containers: vec![],

View File

@@ -37,6 +37,7 @@ impl ToDIDL for Album {
parent_id: parent_id.to_string(),
restricted: Some("1".to_string()),
child_count: self.tracks_count.map(|c| c.to_string()),
searchable: Some("1".to_string()),
title: self.formatted_title(),
class: "object.container.album.musicAlbum".to_string(),
containers: Vec::new(),
@@ -134,6 +135,7 @@ impl ToDIDL for Playlist {
parent_id: parent_id.to_string(),
restricted: Some("1".to_string()),
child_count: self.tracks_count.map(|c| c.to_string()),
searchable: Some("1".to_string()),
title: self.name.clone(),
class: "object.container.playlistContainer".to_string(),
containers: Vec::new(),

View File

@@ -275,6 +275,7 @@ impl MusicSource for QobuzSource {
parent_id: "0".to_string(),
restricted: Some("1".to_string()),
child_count: Some("2".to_string()), // Favorites + Search (simplified)
searchable: Some("1".to_string()),
title: "Qobuz".to_string(),
class: "object.container".to_string(),
containers: vec![
@@ -284,6 +285,7 @@ impl MusicSource for QobuzSource {
parent_id: "qobuz".to_string(),
restricted: Some("1".to_string()),
child_count: None, // Will be determined when browsed
searchable: Some("1".to_string()),
title: "My Favorites".to_string(),
class: "object.container".to_string(),
containers: vec![],

View File

@@ -134,6 +134,8 @@ pub struct SourceRootContainer {
pub class: String,
/// Nombre d'enfants
pub child_count: Option<String>,
/// Indique si le container est searchable ("1" ou "0")
pub searchable: Option<String>,
}
/// Message d'erreur
@@ -639,6 +641,7 @@ async fn get_source_root(Path(id): Path<String>) -> impl IntoResponse {
title: container.title,
class: container.class,
child_count: container.child_count,
searchable: container.searchable,
};
(StatusCode::OK, Json(root)).into_response()
}

View File

@@ -962,6 +962,7 @@ mod tests {
parent_id: "0".to_string(),
restricted: Some("1".to_string()),
child_count: Some("0".to_string()),
searchable: Some("1".to_string()),
title: "Test Source".to_string(),
class: "object.container".to_string(),
containers: vec![],

View File

@@ -979,8 +979,7 @@ impl ServiceInstance {
// Essayer de downcaster vers des types primitifs courants
if let Some(v) = value.as_any().downcast_ref::<String>() {
// Échapper les caractères XML spéciaux
return escape(v).to_string();
return v.clone();
} else if let Some(v) = value.as_any().downcast_ref::<u8>() {
return v.to_string();
} else if let Some(v) = value.as_any().downcast_ref::<u16>() {
@@ -1000,7 +999,7 @@ impl ServiceInstance {
} else if let Some(v) = value.as_any().downcast_ref::<bool>() {
return if *v { "1" } else { "0" }.to_string();
} else if let Some(v) = value.as_any().downcast_ref::<char>() {
return escape(&v.to_string()).to_string();
return v.to_string();
}
// Pour les structures complexes, essayer de sérialiser avec bevy_reflect
@@ -1345,7 +1344,7 @@ async fn control_handler(State(instance): State<Arc<ServiceInstance>>, body: Str
match action_instance_for_run.run(soap_values).await {
Ok(output_data) => {
// Convertir ActionData (Reflect) → HashMap<String, String> pour SOAP
let mut soap_values = HashMap::new();
let mut soap_values: Vec<(String, String)> = Vec::new();
for arg_inst in action_instance.arguments_set().all() {
let arg_model = arg_inst.as_ref().get_model();
@@ -1353,7 +1352,7 @@ async fn control_handler(State(instance): State<Arc<ServiceInstance>>, body: Str
if let Some(reflect_value) = output_data.get(arg_inst.get_name()) {
let soap_string =
ServiceInstance::reflect_to_string(reflect_value.as_ref());
soap_values.insert(arg_inst.get_name().to_string(), soap_string);
soap_values.push((arg_inst.get_name().to_string(), soap_string));
}
}
}

View File

@@ -1,6 +1,5 @@
//! Construction de réponses SOAP
use std::collections::HashMap;
use xmltree::{Element, XMLNode};
/// Construit une réponse SOAP UPnP
@@ -17,13 +16,12 @@ use xmltree::{Element, XMLNode};
pub fn build_soap_response(
service_urn: &str,
action: &str,
values: HashMap<String, String>,
values: Vec<(String, String)>,
) -> Result<String, xmltree::Error> {
// Construire l'élément de réponse
// Format: <u:ActionResponse xmlns:u="service-urn">
let response_name = format!("{}Response", action);
let response_name = format!("u:{}Response", action);
let mut response_elem = Element::new(&response_name);
response_elem.namespace = Some(service_urn.to_string());
response_elem
.attributes
.insert("xmlns:u".to_string(), service_urn.to_string());
@@ -54,6 +52,7 @@ pub fn build_soap_response(
// Sérialiser en XML
let mut buf = Vec::new();
let config = xmltree::EmitterConfig::new()
.write_document_declaration(true)
.perform_indent(true)
.indent_string(" ");
envelope.write_with_config(&mut buf, config)?;
@@ -67,9 +66,9 @@ mod tests {
#[test]
fn test_build_response() {
let mut values = HashMap::new();
values.insert("Track".to_string(), "5".to_string());
values.insert("TrackDuration".to_string(), "00:03:45".to_string());
let mut values = Vec::new();
values.push(("Track".to_string(), "5".to_string()));
values.push(("TrackDuration".to_string(), "00:03:45".to_string()));
let xml = build_soap_response(
"urn:schemas-upnp-org:service:AVTransport:1",
@@ -86,7 +85,7 @@ mod tests {
#[test]
fn test_build_empty_response() {
let values = HashMap::new();
let values = Vec::new();
let xml = build_soap_response("urn:schemas-upnp-org:service:AVTransport:1", "Stop", values)
.unwrap();

View File

@@ -129,6 +129,7 @@ pub fn build_soap_fault(
// Sérialiser
let mut buf = Vec::new();
let config = xmltree::EmitterConfig::new()
.write_document_declaration(true)
.perform_indent(true)
.indent_string(" ");
envelope.write_with_config(&mut buf, config)?;

View File

@@ -39,8 +39,8 @@
//! assert_eq!(action.args.get("InstanceID"), Some(&"0".to_string()));
//!
//! // Construire une réponse
//! let mut values = std::collections::HashMap::new();
//! values.insert("CurrentTrack".to_string(), "5".to_string());
//! let mut values = Vec::new();
//! values.push(("CurrentTrack".to_string(), "5".to_string()));
//! let response = build_soap_response(
//! "urn:schemas-upnp-org:service:AVTransport:1",
//! "GetPositionInfo",