Je ne sais pas trop
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -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
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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![],
|
||||
|
||||
@@ -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![],
|
||||
|
||||
@@ -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![],
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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![],
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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![],
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user