//! # pmodidl - DIDL-Lite Parser //! //! Parser et utilitaires pour le format DIDL-Lite utilisé dans UPnP/DLNA. use bevy_reflect::Reflect; use serde::{Deserialize, Serialize}; use std::fmt::Write; // ============= Couche d'abstraction générique ============= /// Trait pour tout parser de métadonnées média pub trait MediaMetadataParser: Sized { type Error: std::error::Error + Send + Sync + 'static; /// Parse une chaîne de métadonnées fn parse(input: &str) -> Result; /// Retourne le format du parser fn format_name() -> &'static str; } /// Enveloppe générique pour tout type de métadonnées parsées #[derive(Debug, Clone, Serialize, Deserialize, Reflect)] pub struct ParsedMetadata { /// Format du document (ex: "DIDL-Lite", "RSS", etc.) pub format: String, /// Données parsées pub data: T, /// Timestamp du parsing (exclu de la réflexion car SystemTime n'implémente pas Reflect) #[reflect(ignore)] #[serde(skip_serializing_if = "Option::is_none")] pub parsed_at: Option, } impl ParsedMetadata { pub fn new(format: impl Into, data: T) -> Self { Self { format: format.into(), data, parsed_at: Some(std::time::SystemTime::now()), } } /// Transforme les données avec une fonction pub fn map(self, f: F) -> ParsedMetadata where F: FnOnce(T) -> U, { ParsedMetadata { format: self.format, data: f(self.data), parsed_at: self.parsed_at, } } } /// Fonction helper pour parser et envelopper automatiquement pub fn parse_metadata(input: &str) -> Result, P::Error> { let data = P::parse(input)?; Ok(ParsedMetadata::new(P::format_name(), data)) } // ============= Implémentation pour DIDLLite ============= impl MediaMetadataParser for DIDLLite { type Error = quick_xml::de::DeError; fn parse(input: &str) -> Result { quick_xml::de::from_str(input) } fn format_name() -> &'static str { "DIDL-Lite" } } /// Type alias pour faciliter l'utilisation pub type DidlMetadata = ParsedMetadata; // ============= Structures DIDL-Lite ============= /// Racine d'un document DIDL-Lite #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)] #[serde(rename = "DIDL-Lite")] pub struct DIDLLite { #[serde(rename = "@xmlns")] pub xmlns: String, #[serde(rename = "@xmlns:upnp", skip_serializing_if = "Option::is_none")] pub xmlns_upnp: Option, #[serde(rename = "@xmlns:dc", skip_serializing_if = "Option::is_none")] pub xmlns_dc: Option, #[serde(rename = "@xmlns:dlna", skip_serializing_if = "Option::is_none")] pub xmlns_dlna: Option, #[serde(rename = "@xmlns:sec", skip_serializing_if = "Option::is_none")] pub xmlns_sec: Option, #[serde(rename = "@xmlns:pv", skip_serializing_if = "Option::is_none")] pub xmlns_pv: Option, #[serde(rename = "container", default)] pub containers: Vec, #[serde(rename = "item", default)] pub items: Vec, } /// Container pouvant contenir d'autres containers ou items #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)] pub struct Container { #[serde(rename = "@id")] pub id: String, #[serde(rename = "@parentID")] pub parent_id: String, #[serde(rename = "@restricted", skip_serializing_if = "Option::is_none")] pub restricted: Option, #[serde(rename = "@childCount", skip_serializing_if = "Option::is_none")] pub child_count: Option, #[serde(rename = "dc:title", alias = "title")] pub title: String, #[serde(rename = "upnp:class", alias = "class")] pub class: String, #[serde(rename = "container", default)] pub containers: Vec, #[serde(rename = "item", default)] pub items: Vec, } /// Item représentant un objet audio #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)] pub struct Item { #[serde(rename = "@id")] pub id: String, #[serde(rename = "@parentID")] pub parent_id: String, #[serde(rename = "@restricted", skip_serializing_if = "Option::is_none")] pub restricted: Option, #[serde(rename = "dc:title", alias = "title")] pub title: String, #[serde( rename = "dc:creator", alias = "creator", skip_serializing_if = "Option::is_none" )] pub creator: Option, #[serde(rename = "upnp:class", alias = "class")] pub class: String, #[serde( rename = "upnp:artist", alias = "artist", skip_serializing_if = "Option::is_none" )] pub artist: Option, #[serde( rename = "upnp:album", alias = "album", skip_serializing_if = "Option::is_none" )] pub album: Option, #[serde( rename = "upnp:genre", alias = "genre", skip_serializing_if = "Option::is_none" )] pub genre: Option, #[serde( rename = "upnp:albumArtURI", alias = "albumArtURI", skip_serializing_if = "Option::is_none" )] pub album_art: Option, #[serde(skip)] pub album_art_pk: Option, #[serde( rename = "dc:date", alias = "date", skip_serializing_if = "Option::is_none" )] pub date: Option, #[serde( rename = "upnp:originalTrackNumber", alias = "originalTrackNumber", skip_serializing_if = "Option::is_none" )] pub original_track_number: Option, #[serde(rename = "res", default)] pub resources: Vec, #[serde(rename = "desc", default)] pub descriptions: Vec, } /// Ressource média (fichier audio) #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)] pub struct Resource { #[serde(rename = "@protocolInfo")] pub protocol_info: String, #[serde(rename = "@bitsPerSample", skip_serializing_if = "Option::is_none")] pub bits_per_sample: Option, #[serde(rename = "@sampleFrequency", skip_serializing_if = "Option::is_none")] pub sample_frequency: Option, #[serde(rename = "@nrAudioChannels", skip_serializing_if = "Option::is_none")] pub nr_audio_channels: Option, #[serde(rename = "@duration", skip_serializing_if = "Option::is_none")] pub duration: Option, #[serde(rename = "$text")] pub url: String, } /// Description avec métadonnées additionnelles (replaygain, etc.) #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)] pub struct Description { #[serde(rename = "@id", skip_serializing_if = "Option::is_none")] pub id: Option, #[serde(rename = "@nameSpace", skip_serializing_if = "Option::is_none")] pub namespace: Option, #[serde(rename = "track_gain", skip_serializing_if = "Option::is_none")] pub track_gain: Option, #[serde(rename = "track_peak", skip_serializing_if = "Option::is_none")] pub track_peak: Option, } // ============= Implémentation des méthodes ============= impl Default for DIDLLite { fn default() -> Self { Self { xmlns: "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/".to_string(), xmlns_upnp: Some("urn:schemas-upnp-org:metadata-1-0/upnp/".to_string()), xmlns_dc: Some("http://purl.org/dc/elements/1.1/".to_string()), xmlns_dlna: None, xmlns_sec: None, xmlns_pv: None, containers: Vec::new(), items: Vec::new(), } } } impl DIDLLite { /// Itère sur tous les containers de manière récursive pub fn all_containers(&self) -> impl Iterator { AllContainersIter::new(&self.containers) } /// Itère sur tous les items de manière récursive pub fn all_items(&self) -> impl Iterator { AllItemsIter::new(&self.containers, &self.items) } /// Trouve un container par ID pub fn get_container_by_id(&self, id: &str) -> Option<&Container> { self.all_containers().find(|c| c.id == id) } /// Trouve un item par ID pub fn get_item_by_id(&self, id: &str) -> Option<&Item> { self.all_items().find(|i| i.id == id) } /// Filtre les containers pub fn filter_containers(&self, predicate: F) -> impl Iterator where F: Fn(&Container) -> bool, { self.all_containers().filter(move |c| predicate(c)) } /// Filtre les items pub fn filter_items(&self, predicate: F) -> impl Iterator where F: Fn(&Item) -> bool, { self.all_items().filter(move |i| predicate(i)) } /// Génère une représentation Markdown pub fn to_markdown(&self) -> String { let mut buf = String::new(); buf.push_str("### DIDL-Lite Document\n\n"); if !self.containers.is_empty() { buf.push_str("#### Containers\n\n"); for container in &self.containers { container.write_markdown(&mut buf, 0); } } if !self.items.is_empty() { buf.push_str("#### Items\n\n"); for item in &self.items { item.write_markdown(&mut buf, 0); } } buf } } impl Container { /// Itère sur tous les containers enfants récursivement pub fn all_containers(&self) -> impl Iterator { AllContainersIter::new(&self.containers) } /// Itère sur tous les items de ce container et ses enfants pub fn all_items(&self) -> impl Iterator { AllItemsIter::new(&self.containers, &self.items) } fn write_markdown(&self, buf: &mut String, depth: usize) { let indent = " ".repeat(depth); writeln!(buf, "{}- **Container**: {}", indent, self.title).unwrap(); writeln!(buf, "{} - ID: `{}`", indent, self.id).unwrap(); writeln!(buf, "{} - ParentID: `{}`", indent, self.parent_id).unwrap(); writeln!(buf, "{} - Class: `{}`", indent, self.class).unwrap(); if let Some(ref restricted) = self.restricted { writeln!(buf, "{} - Restricted: `{}`", indent, restricted).unwrap(); } if let Some(ref count) = self.child_count { writeln!(buf, "{} - ChildCount: `{}`", indent, count).unwrap(); } if !self.containers.is_empty() { writeln!(buf, "{} - Subcontainers:", indent).unwrap(); for sub in &self.containers { sub.write_markdown(buf, depth + 2); } } if !self.items.is_empty() { writeln!(buf, "{} - Items:", indent).unwrap(); for item in &self.items { item.write_markdown(buf, depth + 2); } } buf.push('\n'); } } impl Item { /// Itère sur les ressources audio uniquement pub fn audio_resources(&self) -> impl Iterator { self.resources .iter() .filter(|r| r.protocol_info.contains("audio/")) } /// Retourne la ressource principale (première disponible) pub fn primary_resource(&self) -> Option<&Resource> { self.resources.first() } /// Itère sur les métadonnées sous forme de paires clé-valeur pub fn metadata(&self) -> impl Iterator { let mut pairs = Vec::new(); pairs.push(("title", self.title.as_str())); if let Some(ref artist) = self.artist { pairs.push(("artist", artist.as_str())); } if let Some(ref album) = self.album { pairs.push(("album", album.as_str())); } if let Some(ref genre) = self.genre { pairs.push(("genre", genre.as_str())); } if let Some(ref date) = self.date { pairs.push(("date", date.as_str())); } if let Some(ref track) = self.original_track_number { pairs.push(("trackNumber", track.as_str())); } for desc in &self.descriptions { if let Some(ref gain) = desc.track_gain { pairs.push(("replayGain", gain.as_str())); } if let Some(ref peak) = desc.track_peak { pairs.push(("replayPeak", peak.as_str())); } } pairs.into_iter() } fn write_markdown(&self, buf: &mut String, depth: usize) { let indent = " ".repeat(depth); writeln!(buf, "{}- **Item**: {}", indent, self.title).unwrap(); writeln!(buf, "{} - ID: `{}`", indent, self.id).unwrap(); writeln!(buf, "{} - ParentID: `{}`", indent, self.parent_id).unwrap(); writeln!(buf, "{} - Class: `{}`", indent, self.class).unwrap(); if let Some(ref creator) = self.creator { writeln!(buf, "{} - Creator: {}", indent, creator).unwrap(); } if let Some(ref artist) = self.artist { writeln!(buf, "{} - Artist: {}", indent, artist).unwrap(); } if let Some(ref album) = self.album { writeln!(buf, "{} - Album: {}", indent, album).unwrap(); } if let Some(ref genre) = self.genre { writeln!(buf, "{} - Genre: {}", indent, genre).unwrap(); } if let Some(ref art) = self.album_art { writeln!(buf, "{} - Album Art: ![Cover]({})", indent, art).unwrap(); } if let Some(ref date) = self.date { writeln!(buf, "{} - Date: {}", indent, date).unwrap(); } if let Some(ref track) = self.original_track_number { writeln!(buf, "{} - Track: {}", indent, track).unwrap(); } if !self.resources.is_empty() { writeln!(buf, "{} - Resources:", indent).unwrap(); for res in &self.resources { writeln!(buf, "{} - URL: {}", indent, res.url).unwrap(); writeln!(buf, "{} - Protocol: `{}`", indent, res.protocol_info).unwrap(); if let Some(ref dur) = res.duration { writeln!(buf, "{} - Duration: `{}`", indent, dur).unwrap(); } if let Some(ref bits) = res.bits_per_sample { writeln!(buf, "{} - BitsPerSample: `{}`", indent, bits).unwrap(); } if let Some(ref freq) = res.sample_frequency { writeln!(buf, "{} - SampleFrequency: `{}`", indent, freq).unwrap(); } if let Some(ref channels) = res.nr_audio_channels { writeln!(buf, "{} - Channels: `{}`", indent, channels).unwrap(); } } } if !self.descriptions.is_empty() { writeln!(buf, "{} - Descriptions:", indent).unwrap(); for desc in &self.descriptions { if let Some(ref ns) = desc.namespace { writeln!(buf, "{} - Namespace: `{}`", indent, ns).unwrap(); } if let Some(ref gain) = desc.track_gain { writeln!(buf, "{} - Track Gain: `{}`", indent, gain).unwrap(); } if let Some(ref peak) = desc.track_peak { writeln!(buf, "{} - Track Peak: `{}`", indent, peak).unwrap(); } } } buf.push('\n'); } } // ============= Itérateurs personnalisés ============= struct AllContainersIter<'a> { stack: Vec<&'a Container>, } impl<'a> AllContainersIter<'a> { fn new(containers: &'a [Container]) -> Self { Self { stack: containers.iter().collect(), } } } impl<'a> Iterator for AllContainersIter<'a> { type Item = &'a Container; fn next(&mut self) -> Option { self.stack.pop().map(|container| { // Ajouter les enfants à la pile self.stack.extend(container.containers.iter()); container }) } } struct AllItemsIter<'a> { containers: Vec<&'a Container>, current_items: std::slice::Iter<'a, Item>, } impl<'a> AllItemsIter<'a> { fn new(containers: &'a [Container], items: &'a [Item]) -> Self { Self { containers: containers.iter().collect(), current_items: items.iter(), } } } impl<'a> Iterator for AllItemsIter<'a> { type Item = &'a Item; fn next(&mut self) -> Option { loop { if let Some(item) = self.current_items.next() { return Some(item); } let container = self.containers.pop()?; self.containers.extend(container.containers.iter()); self.current_items = container.items.iter(); } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_simple_didl() { let xml = r#" Test Song object.item.audioItem.musicTrack http://example.com/song.mp3 "#; let didl = DIDLLite::parse(xml).unwrap(); assert_eq!(didl.items.len(), 1); assert_eq!(didl.items[0].title, "Test Song"); } #[test] fn test_parse_without_namespaces() { // Teste un XML sans namespaces explicites (devices UPnP laxistes) let xml = r#" Test Song object.item.audioItem.musicTrack http://example.com/song.mp3 "#; let didl = DIDLLite::parse(xml).unwrap(); assert_eq!(didl.items.len(), 1); assert_eq!(didl.items[0].title, "Test Song"); } #[test] fn test_generic_parser() { let xml = r#" "#; // Utiliser le parser générique let metadata: DidlMetadata = parse_metadata(xml).unwrap(); assert_eq!(metadata.format, "DIDL-Lite"); assert!(metadata.parsed_at.is_some()); } #[test] fn test_metadata_map() { let xml = r#" "#; let metadata: DidlMetadata = parse_metadata(xml).unwrap(); // Transformer les données let item_count = metadata.map(|didl| didl.items.len()); assert_eq!(item_count.format, "DIDL-Lite"); assert_eq!(item_count.data, 0); } }