amélioration de la webapp
This commit is contained in:
@@ -2,19 +2,19 @@
|
||||
//!
|
||||
//! Parser et utilitaires pour le format DIDL-Lite utilisé dans UPnP/DLNA.
|
||||
|
||||
use bevy_reflect::Reflect;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
use bevy_reflect::Reflect;
|
||||
|
||||
// ============= 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<Self, Self::Error>;
|
||||
|
||||
|
||||
/// Retourne le format du parser
|
||||
fn format_name() -> &'static str;
|
||||
}
|
||||
@@ -24,10 +24,10 @@ pub trait MediaMetadataParser: Sized {
|
||||
pub struct ParsedMetadata<T> {
|
||||
/// 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")]
|
||||
@@ -42,7 +42,7 @@ impl<T> ParsedMetadata<T> {
|
||||
parsed_at: Some(std::time::SystemTime::now()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Transforme les données avec une fonction
|
||||
pub fn map<U, F>(self, f: F) -> ParsedMetadata<U>
|
||||
where
|
||||
@@ -66,11 +66,11 @@ pub fn parse_metadata<P: MediaMetadataParser>(input: &str) -> Result<ParsedMetad
|
||||
|
||||
impl MediaMetadataParser for DIDLLite {
|
||||
type Error = quick_xml::de::DeError;
|
||||
|
||||
|
||||
fn parse(input: &str) -> Result<Self, Self::Error> {
|
||||
quick_xml::de::from_str(input)
|
||||
}
|
||||
|
||||
|
||||
fn format_name() -> &'static str {
|
||||
"DIDL-Lite"
|
||||
}
|
||||
@@ -81,32 +81,31 @@ pub type DidlMetadata = ParsedMetadata<DIDLLite>;
|
||||
|
||||
// ============= 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<String>,
|
||||
|
||||
|
||||
#[serde(rename = "@xmlns:dc", skip_serializing_if = "Option::is_none")]
|
||||
pub xmlns_dc: Option<String>,
|
||||
|
||||
|
||||
#[serde(rename = "@xmlns:dlna", skip_serializing_if = "Option::is_none")]
|
||||
pub xmlns_dlna: Option<String>,
|
||||
|
||||
|
||||
#[serde(rename = "@xmlns:sec", skip_serializing_if = "Option::is_none")]
|
||||
pub xmlns_sec: Option<String>,
|
||||
|
||||
|
||||
#[serde(rename = "@xmlns:pv", skip_serializing_if = "Option::is_none")]
|
||||
pub xmlns_pv: Option<String>,
|
||||
|
||||
|
||||
#[serde(rename = "container", default)]
|
||||
pub containers: Vec<Container>,
|
||||
|
||||
|
||||
#[serde(rename = "item", default)]
|
||||
pub items: Vec<Item>,
|
||||
}
|
||||
@@ -116,25 +115,25 @@ pub struct DIDLLite {
|
||||
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<String>,
|
||||
|
||||
|
||||
#[serde(rename = "@childCount", skip_serializing_if = "Option::is_none")]
|
||||
pub child_count: Option<String>,
|
||||
|
||||
|
||||
#[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<Container>,
|
||||
|
||||
|
||||
#[serde(rename = "item", default)]
|
||||
pub items: Vec<Item>,
|
||||
}
|
||||
@@ -144,46 +143,74 @@ pub struct Container {
|
||||
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<String>,
|
||||
|
||||
|
||||
#[serde(rename = "dc:title", alias = "title")]
|
||||
pub title: String,
|
||||
|
||||
#[serde(rename = "dc:creator", alias = "creator", skip_serializing_if = "Option::is_none")]
|
||||
|
||||
#[serde(
|
||||
rename = "dc:creator",
|
||||
alias = "creator",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub creator: Option<String>,
|
||||
|
||||
|
||||
#[serde(rename = "upnp:class", alias = "class")]
|
||||
pub class: String,
|
||||
|
||||
#[serde(rename = "upnp:artist", alias = "artist", skip_serializing_if = "Option::is_none")]
|
||||
|
||||
#[serde(
|
||||
rename = "upnp:artist",
|
||||
alias = "artist",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub artist: Option<String>,
|
||||
|
||||
#[serde(rename = "upnp:album", alias = "album", skip_serializing_if = "Option::is_none")]
|
||||
|
||||
#[serde(
|
||||
rename = "upnp:album",
|
||||
alias = "album",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub album: Option<String>,
|
||||
|
||||
#[serde(rename = "upnp:genre", alias = "genre", skip_serializing_if = "Option::is_none")]
|
||||
|
||||
#[serde(
|
||||
rename = "upnp:genre",
|
||||
alias = "genre",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub genre: Option<String>,
|
||||
|
||||
#[serde(rename = "upnp:albumArtURI", alias = "albumArtURI", skip_serializing_if = "Option::is_none")]
|
||||
|
||||
#[serde(
|
||||
rename = "upnp:albumArtURI",
|
||||
alias = "albumArtURI",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub album_art: Option<String>,
|
||||
|
||||
|
||||
#[serde(skip)]
|
||||
pub album_art_pk: Option<String>,
|
||||
|
||||
#[serde(rename = "dc:date", alias = "date", skip_serializing_if = "Option::is_none")]
|
||||
|
||||
#[serde(
|
||||
rename = "dc:date",
|
||||
alias = "date",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub date: Option<String>,
|
||||
|
||||
#[serde(rename = "upnp:originalTrackNumber", alias = "originalTrackNumber", skip_serializing_if = "Option::is_none")]
|
||||
|
||||
#[serde(
|
||||
rename = "upnp:originalTrackNumber",
|
||||
alias = "originalTrackNumber",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub original_track_number: Option<String>,
|
||||
|
||||
|
||||
#[serde(rename = "res", default)]
|
||||
pub resources: Vec<Resource>,
|
||||
|
||||
|
||||
#[serde(rename = "desc", default)]
|
||||
pub descriptions: Vec<Description>,
|
||||
}
|
||||
@@ -193,19 +220,19 @@ pub struct Item {
|
||||
pub struct Resource {
|
||||
#[serde(rename = "@protocolInfo")]
|
||||
pub protocol_info: String,
|
||||
|
||||
|
||||
#[serde(rename = "@bitsPerSample", skip_serializing_if = "Option::is_none")]
|
||||
pub bits_per_sample: Option<String>,
|
||||
|
||||
|
||||
#[serde(rename = "@sampleFrequency", skip_serializing_if = "Option::is_none")]
|
||||
pub sample_frequency: Option<String>,
|
||||
|
||||
|
||||
#[serde(rename = "@nrAudioChannels", skip_serializing_if = "Option::is_none")]
|
||||
pub nr_audio_channels: Option<String>,
|
||||
|
||||
|
||||
#[serde(rename = "@duration", skip_serializing_if = "Option::is_none")]
|
||||
pub duration: Option<String>,
|
||||
|
||||
|
||||
#[serde(rename = "$text")]
|
||||
pub url: String,
|
||||
}
|
||||
@@ -215,13 +242,13 @@ pub struct Resource {
|
||||
pub struct Description {
|
||||
#[serde(rename = "@id", skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<String>,
|
||||
|
||||
|
||||
#[serde(rename = "@nameSpace", skip_serializing_if = "Option::is_none")]
|
||||
pub namespace: Option<String>,
|
||||
|
||||
|
||||
#[serde(rename = "track_gain", skip_serializing_if = "Option::is_none")]
|
||||
pub track_gain: Option<String>,
|
||||
|
||||
|
||||
#[serde(rename = "track_peak", skip_serializing_if = "Option::is_none")]
|
||||
pub track_peak: Option<String>,
|
||||
}
|
||||
@@ -233,22 +260,22 @@ impl DIDLLite {
|
||||
pub fn all_containers(&self) -> impl Iterator<Item = &Container> {
|
||||
AllContainersIter::new(&self.containers)
|
||||
}
|
||||
|
||||
|
||||
/// Itère sur tous les items de manière récursive
|
||||
pub fn all_items(&self) -> impl Iterator<Item = &Item> {
|
||||
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<F>(&self, predicate: F) -> impl Iterator<Item = &Container>
|
||||
where
|
||||
@@ -256,7 +283,7 @@ impl DIDLLite {
|
||||
{
|
||||
self.all_containers().filter(move |c| predicate(c))
|
||||
}
|
||||
|
||||
|
||||
/// Filtre les items
|
||||
pub fn filter_items<F>(&self, predicate: F) -> impl Iterator<Item = &Item>
|
||||
where
|
||||
@@ -264,26 +291,26 @@ impl DIDLLite {
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -293,41 +320,41 @@ impl Container {
|
||||
pub fn all_containers(&self) -> impl Iterator<Item = &Container> {
|
||||
AllContainersIter::new(&self.containers)
|
||||
}
|
||||
|
||||
|
||||
/// Itère sur tous les items de ce container et ses enfants
|
||||
pub fn all_items(&self) -> impl Iterator<Item = &Item> {
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -335,21 +362,22 @@ impl Container {
|
||||
impl Item {
|
||||
/// Itère sur les ressources audio uniquement
|
||||
pub fn audio_resources(&self) -> impl Iterator<Item = &Resource> {
|
||||
self.resources.iter()
|
||||
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<Item = (&str, &str)> {
|
||||
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()));
|
||||
}
|
||||
@@ -365,7 +393,7 @@ impl Item {
|
||||
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()));
|
||||
@@ -374,18 +402,18 @@ impl Item {
|
||||
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();
|
||||
}
|
||||
@@ -407,7 +435,7 @@ impl Item {
|
||||
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 {
|
||||
@@ -427,7 +455,7 @@ impl Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if !self.descriptions.is_empty() {
|
||||
writeln!(buf, "{} - Descriptions:", indent).unwrap();
|
||||
for desc in &self.descriptions {
|
||||
@@ -442,7 +470,7 @@ impl Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
buf.push('\n');
|
||||
}
|
||||
}
|
||||
@@ -463,7 +491,7 @@ impl<'a> AllContainersIter<'a> {
|
||||
|
||||
impl<'a> Iterator for AllContainersIter<'a> {
|
||||
type Item = &'a Container;
|
||||
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.stack.pop().map(|container| {
|
||||
// Ajouter les enfants à la pile
|
||||
@@ -489,13 +517,13 @@ impl<'a> AllItemsIter<'a> {
|
||||
|
||||
impl<'a> Iterator for AllItemsIter<'a> {
|
||||
type Item = &'a Item;
|
||||
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
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();
|
||||
@@ -506,7 +534,7 @@ impl<'a> Iterator for AllItemsIter<'a> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_parse_simple_didl() {
|
||||
let xml = r#"
|
||||
@@ -520,12 +548,12 @@ mod tests {
|
||||
</item>
|
||||
</DIDL-Lite>
|
||||
"#;
|
||||
|
||||
|
||||
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)
|
||||
@@ -538,12 +566,12 @@ mod tests {
|
||||
</item>
|
||||
</DIDL-Lite>
|
||||
"#;
|
||||
|
||||
|
||||
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#"
|
||||
@@ -552,14 +580,14 @@ mod tests {
|
||||
xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">
|
||||
</DIDL-Lite>
|
||||
"#;
|
||||
|
||||
|
||||
// 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#"
|
||||
@@ -568,13 +596,13 @@ mod tests {
|
||||
xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">
|
||||
</DIDL-Lite>
|
||||
"#;
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user