From 32064e3f12e32acd8e1bf92749c9b9aaaa0ebc4c Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Wed, 1 Oct 2025 20:49:50 +0200 Subject: [PATCH] =?UTF-8?q?Comen=C3=A7ons=20un=20Mediarenderer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 353 +++++++++++ Cargo.toml | 2 +- PMOMusic/src/main.rs | 10 +- pmodidl/Cargo.toml | 12 + pmodidl/src/lib.rs | 580 ++++++++++++++++++ pmoupnp/Cargo.toml | 3 + pmoupnp/src/actions/macros.rs | 197 ++++++ pmoupnp/src/actions/mod.rs | 4 +- pmoupnp/src/lib.rs | 2 + .../mediarenderer/avtransport/actions/mod.rs | 8 + .../mediarenderer/avtransport/actions/play.rs | 19 + .../avtransport/actions/setavtransporturi.rs | 10 + .../mediarenderer/avtransport/actions/stop.rs | 8 + pmoupnp/src/mediarenderer/avtransport/mod.rs | 3 + .../variables/a_arg_type_instanceid.rs | 7 + .../variables/a_arg_type_playspeed.rs | 7 + .../avtransport/variables/avtransporturi.rs | 7 + .../variables/avtransporturimetadata.rs | 33 + .../variables/currenttrackduration.rs | 8 + .../avtransport/variables/mod.rs | 22 + .../avtransport/variables/seekmode.rs | 8 + .../variables/transportplayspeed.rs | 13 + .../avtransport/variables/transportstate.rs | 21 + .../avtransport/variables/transportstatus.rs | 17 + pmoupnp/src/mediarenderer/mod.rs | 1 + pmoupnp/src/object_trait.rs | 68 +- pmoupnp/src/server/mod.rs | 73 ++- pmoupnp/src/state_variables/mod.rs | 3 +- .../src/state_variables/variable_methods.rs | 4 +- pmoupnp/src/variable_types/mod.rs | 2 + pmoupnp/src/variable_types/values_from_str.rs | 21 + pmoupnp/webapp/src/components/LogView.vue | 1 + 32 files changed, 1485 insertions(+), 42 deletions(-) create mode 100644 pmodidl/Cargo.toml create mode 100644 pmodidl/src/lib.rs create mode 100644 pmoupnp/src/actions/macros.rs create mode 100644 pmoupnp/src/mediarenderer/avtransport/actions/mod.rs create mode 100644 pmoupnp/src/mediarenderer/avtransport/actions/play.rs create mode 100644 pmoupnp/src/mediarenderer/avtransport/actions/setavtransporturi.rs create mode 100644 pmoupnp/src/mediarenderer/avtransport/actions/stop.rs create mode 100644 pmoupnp/src/mediarenderer/avtransport/mod.rs create mode 100644 pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_instanceid.rs create mode 100644 pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_playspeed.rs create mode 100644 pmoupnp/src/mediarenderer/avtransport/variables/avtransporturi.rs create mode 100644 pmoupnp/src/mediarenderer/avtransport/variables/avtransporturimetadata.rs create mode 100644 pmoupnp/src/mediarenderer/avtransport/variables/currenttrackduration.rs create mode 100644 pmoupnp/src/mediarenderer/avtransport/variables/mod.rs create mode 100644 pmoupnp/src/mediarenderer/avtransport/variables/seekmode.rs create mode 100644 pmoupnp/src/mediarenderer/avtransport/variables/transportplayspeed.rs create mode 100644 pmoupnp/src/mediarenderer/avtransport/variables/transportstate.rs create mode 100644 pmoupnp/src/mediarenderer/avtransport/variables/transportstatus.rs create mode 100644 pmoupnp/src/mediarenderer/mod.rs create mode 100644 pmoupnp/src/variable_types/values_from_str.rs diff --git a/Cargo.lock b/Cargo.lock index 75ba6639..47cdef5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,6 +63,17 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "assert_type_match" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f548ad2c4031f2902e3edc1f29c29e835829437de49562d8eb5dc5584d3a1043" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -231,11 +242,103 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bevy_macro_utils" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61e64e7431f7349b5caa1ea4ff2efd94afd73480fac2f44a2b94b1642b4a02a" +dependencies = [ + "parking_lot", + "proc-macro2", + "quote", + "syn", + "toml_edit", +] + +[[package]] +name = "bevy_platform" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239c3f54e9e25a01e4d0f54a3a066e1d8d6d5509b071a78c729c1e0143db7f4b" +dependencies = [ + "critical-section", + "foldhash", + "futures-channel", + "hashbrown", + "js-sys", + "portable-atomic", + "portable-atomic-util", + "serde", + "spin", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "bevy_ptr" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b3533a9a9c2158bc704c18ba26f0f0692d0423ff0a10b656b8db46a9b953620" + +[[package]] +name = "bevy_reflect" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18f363a1e984a5b9e908e9b98181e396d4a93ee8209a698c0d0e38227f464e45" +dependencies = [ + "assert_type_match", + "bevy_platform", + "bevy_ptr", + "bevy_reflect_derive", + "bevy_utils", + "derive_more", + "disqualified", + "downcast-rs", + "erased-serde", + "foldhash", + "glam", + "inventory", + "serde", + "smallvec", + "smol_str", + "thiserror", + "uuid", + "variadics_please", + "wgpu-types", +] + +[[package]] +name = "bevy_reflect_derive" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65b08cc2fb06b872143c506ee7e14decb3ade19bf25af9ddd39dfc1799ccc85" +dependencies = [ + "bevy_macro_utils", + "indexmap", + "proc-macro2", + "quote", + "syn", + "uuid", +] + +[[package]] +name = "bevy_utils" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6927fae6678c8ca2984b97d1a77bb244cfe095f5fd28ed2f88e0d66cfab938fa" +dependencies = [ + "bevy_platform", + "disqualified", +] + [[package]] name = "bitflags" version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +dependencies = [ + "serde", +] [[package]] name = "block-buffer" @@ -252,6 +355,26 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "bytemuck" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "bytes" version = "1.10.1" @@ -318,6 +441,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crypto-common" version = "0.1.6" @@ -374,6 +503,26 @@ dependencies = [ "syn", ] +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -416,12 +565,35 @@ dependencies = [ "syn", ] +[[package]] +name = "disqualified" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9c272297e804878a2a4b707cfcfc6d2328b5bb936944613b4fdf2b9269afdfd" + +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "find-msvc-tools" version = "0.1.2" @@ -445,6 +617,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -620,6 +798,15 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "glam" +version = "0.30.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12d847aeb25f41be4c0ec9587d624e9cd631bc007a8fd7ce3f5851e064c6460" +dependencies = [ + "serde_core", +] + [[package]] name = "h2" version = "0.4.12" @@ -644,6 +831,10 @@ name = "hashbrown" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +dependencies = [ + "equivalent", + "serde", +] [[package]] name = "hex" @@ -884,6 +1075,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inventory" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +dependencies = [ + "rustversion", +] + [[package]] name = "io-uring" version = "0.7.10" @@ -1116,6 +1316,18 @@ dependencies = [ "uuid", ] +[[package]] +name = "pmodidl" +version = "0.1.0" +dependencies = [ + "bevy_reflect", + "bevy_reflect_derive", + "quick-xml", + "serde", + "utoipa", + "utoipa-swagger-ui", +] + [[package]] name = "pmoupnp" version = "0.1.0" @@ -1126,6 +1338,8 @@ dependencies = [ "axum-embed", "axum-server", "base64", + "bevy_reflect", + "bevy_reflect_derive", "chrono", "futures", "futures-util", @@ -1135,6 +1349,7 @@ dependencies = [ "once_cell", "parking_lot", "pmoconfig", + "pmodidl", "rust-embed", "serde", "serde_json", @@ -1158,6 +1373,21 @@ dependencies = [ "get_if_addrs", ] +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.3" @@ -1198,6 +1428,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quote" version = "1.0.41" @@ -1461,6 +1701,15 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + [[package]] name = "socket2" version = "0.6.0" @@ -1471,6 +1720,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +dependencies = [ + "portable-atomic", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1604,6 +1862,36 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml_datetime" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +dependencies = [ + "winnow", +] + [[package]] name = "tower" version = "0.5.2" @@ -1694,6 +1982,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.18.0" @@ -1786,6 +2080,7 @@ checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.3", "js-sys", + "serde", "wasm-bindgen", ] @@ -1825,6 +2120,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "variadics_please" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "version_check" version = "0.9.5" @@ -1892,6 +2198,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.104" @@ -1924,6 +2243,31 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wgpu-types" +version = "26.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca7a8d8af57c18f57d393601a1fb159ace8b2328f1b6b5f80893f7d672c9ae2" +dependencies = [ + "bitflags", + "bytemuck", + "js-sys", + "log", + "serde", + "thiserror", + "web-sys", +] + [[package]] name = "winapi" version = "0.2.8" @@ -2089,6 +2433,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/Cargo.toml b/Cargo.toml index 54434dea..ea24bbb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "3" -members = ["PMOMusic", "pmoupnp","pmoconfig", "pmoutils"] +members = ["PMOMusic", "pmoupnp","pmoconfig", "pmoutils", "pmodidl"] diff --git a/PMOMusic/src/main.rs b/PMOMusic/src/main.rs index 91e68538..7af101bc 100644 --- a/PMOMusic/src/main.rs +++ b/PMOMusic/src/main.rs @@ -1,9 +1,9 @@ -use pmoupnp::server::{ - ServerBuilder, Webapp, - logs::{LogState, SseLayer, log_dump, log_sse}, -}; // ton module pmoupnp::server +use pmoupnp::{mediarenderer::avtransport::actions::{SETAVTRANSPORTURI}, server::{ + logs::{log_dump, log_sse, LogState, SseLayer}, ServerBuilder, Webapp +}, UpnpXml}; // ton module pmoupnp::server use tracing_subscriber::Registry; use tracing_subscriber::prelude::*; +use tracing::info; #[tokio::main] async fn main() { @@ -47,6 +47,8 @@ async fn main() { server.add_redirect("/", "/app").await; + info!("{}",SETAVTRANSPORTURI.to_markdown()); + server.start().await; server.wait().await; } diff --git a/pmodidl/Cargo.toml b/pmodidl/Cargo.toml new file mode 100644 index 00000000..3ed5c8a0 --- /dev/null +++ b/pmodidl/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "pmodidl" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde = "1.0.228" +utoipa = { version = "5.4.0", features = ["axum_extras"] } +utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } +quick-xml = { version = "0.38.3", features = ["serialize"] } +bevy_reflect = "0.17.1" +bevy_reflect_derive = "0.17.1" diff --git a/pmodidl/src/lib.rs b/pmodidl/src/lib.rs new file mode 100644 index 00000000..9187ae5d --- /dev/null +++ b/pmodidl/src/lib.rs @@ -0,0 +1,580 @@ +//! # pmodidl - DIDL-Lite Parser +//! +//! Parser et utilitaires pour le format DIDL-Lite utilisé dans UPnP/DLNA. + +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; + + /// 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 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); + } +} \ No newline at end of file diff --git a/pmoupnp/Cargo.toml b/pmoupnp/Cargo.toml index 6f7e32b1..b01e6884 100644 --- a/pmoupnp/Cargo.toml +++ b/pmoupnp/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] pmoconfig = { path = "../pmoconfig" } +pmodidl = { path = "../pmodidl"} url = "2.5.7" uuid = "1.18.1" @@ -34,3 +35,5 @@ anyhow = "1.0" utoipa = { version = "5.4.0", features = ["axum_extras"] } utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } validator = { version = "0.20.0", features = ["derive"] } +bevy_reflect = "0.17.1" +bevy_reflect_derive = "0.17.1" diff --git a/pmoupnp/src/actions/macros.rs b/pmoupnp/src/actions/macros.rs new file mode 100644 index 00000000..9d8e2e6b --- /dev/null +++ b/pmoupnp/src/actions/macros.rs @@ -0,0 +1,197 @@ +/// Macro pour définir facilement des actions UPnP +/// +/// # Syntaxe +/// +/// ```ignore +/// define_action! { +/// pub static ACTION_NAME = "ActionName" { +/// in "ParamName" => VARIABLE_REF, +/// in "OtherParam" => OTHER_VAR, +/// out "ResultParam" => RESULT_VAR, +/// } +/// } +/// ``` +#[macro_export] +macro_rules! define_action { + // Variante sans arguments + (pub static $name:ident = $action_name:literal) => { + pub static $name: once_cell::sync::Lazy<$crate::actions::Action> = + once_cell::sync::Lazy::new(|| { + $crate::actions::Action::new($action_name.to_string()) + }); + }; + + // Variante avec arguments + (pub static $name:ident = $action_name:literal { + $( + $direction:ident $arg_name:literal => $var_ref:expr + ),* $(,)? + }) => { + pub static $name: once_cell::sync::Lazy<$crate::actions::Action> = + once_cell::sync::Lazy::new(|| { + let mut ac = $crate::actions::Action::new($action_name.to_string()); + + $( + ac.add_argument( + define_action!(@arg $direction $arg_name, $var_ref) + ); + )* + + ac + }); + }; + + // Helpers internes pour créer les arguments + (@arg in $name:literal, $var:expr) => { + $crate::actions::Argument::new_in($name.to_string(), $var.clone()) + }; + + (@arg out $name:literal, $var:expr) => { + $crate::actions::Argument::new_out($name.to_string(), $var.clone()) + }; +} + +// ============= Exemples d'utilisation ============= + +#[cfg(test)] +mod examples { + use super::*; + + // Utilisation originale (pour comparaison) + mod original { + use crate::mediarenderer::avtransport::variables::{ + A_ARG_TYPE_INSTANCE_ID, + TRANSPORTPLAYSPEED + }; + use crate::actions::{Action, Argument}; + use once_cell::sync::Lazy; + + pub static PLAY: Lazy = Lazy::new(|| -> Action { + let mut ac = Action::new("Play".to_string()); + + ac.add_argument( + Argument::new_in("InstanceID".to_string(), + A_ARG_TYPE_INSTANCE_ID.clone()) + ); + + ac.add_argument( + Argument::new_in("Speed".to_string(), + TRANSPORTPLAYSPEED.clone()) + ); + + ac + }); + } + + // Avec la macro - Version simple + mod with_macro { + use crate::mediarenderer::avtransport::variables::*; + + define_action! { + pub static PLAY = "Play" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + in "Speed" => TRANSPORTPLAYSPEED, + } + } + + define_action! { + pub static SETAVTRANSPORTURI = "SetAVTransportURI" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + in "CurrentURI" => AVTRANSPORTURI, + in "CurrentURIMetaData" => AVTRANSPORTURIMETADATA, + } + } + + define_action! { + pub static STOP = "Stop" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + } + } + + // Action sans arguments + define_action! { + pub static PAUSE = "Pause" + } + + } +} + +// ============= Macro alternative encore plus concise ============= + +/// Macro pour définir plusieurs actions en une fois +/// +/// # Syntaxe +/// +/// ```ignore +/// define_actions! { +/// PLAY = "Play" { +/// in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, +/// in "Speed" => TRANSPORTPLAYSPEED, +/// } +/// +/// STOP = "Stop" { +/// in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, +/// } +/// } +/// ``` +#[macro_export] +macro_rules! define_actions { + ( + $( + $name:ident = $action_name:literal { + $( + $direction:ident $arg_name:literal => $var_ref:expr + ),* $(,)? + } + )* + ) => { + $( + define_action! { + pub static $name = $action_name { + $($direction $arg_name => $var_ref),* + } + } + )* + }; + + // Support pour actions sans arguments + ( + $( + $name:ident = $action_name:literal + )* + ) => { + $( + define_action! { + pub static $name = $action_name + } + )* + }; +} + +// ============= Exemple d'utilisation groupée ============= + +#[cfg(test)] +mod grouped_example { + use crate::mediarenderer::avtransport::variables::*; + + define_actions! { + PLAY = "Play" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + in "Speed" => TRANSPORTPLAYSPEED, + } + + STOP = "Stop" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + } + + PAUSE = "Pause" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + } + + SETAVTRANSPORTURI = "SetAVTransportURI" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + in "CurrentURI" => AVTRANSPORTURI, + in "CurrentURIMetaData" => AVTRANSPORTURIMETADATA, + } + } +} \ No newline at end of file diff --git a/pmoupnp/src/actions/mod.rs b/pmoupnp/src/actions/mod.rs index 9a765134..8a230945 100644 --- a/pmoupnp/src/actions/mod.rs +++ b/pmoupnp/src/actions/mod.rs @@ -7,13 +7,15 @@ mod action_instance_set; mod argument; mod arg_set_methods; +mod macros; + use std::collections::HashMap; use crate::{state_variables::StateVariable, UpnpObjectType}; #[derive(Debug, Clone)] -struct Action { +pub struct Action { object: UpnpObjectType, arguments: ArgumentSet, } diff --git a/pmoupnp/src/lib.rs b/pmoupnp/src/lib.rs index 8a2480a6..38d1db34 100644 --- a/pmoupnp/src/lib.rs +++ b/pmoupnp/src/lib.rs @@ -6,6 +6,8 @@ pub mod value_ranges; pub mod variable_types; pub mod actions; +pub mod mediarenderer; + pub use crate::object_trait::{UpnpXml,UpnpObject}; #[derive(Debug,Clone)] diff --git a/pmoupnp/src/mediarenderer/avtransport/actions/mod.rs b/pmoupnp/src/mediarenderer/avtransport/actions/mod.rs new file mode 100644 index 00000000..841eb55a --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/actions/mod.rs @@ -0,0 +1,8 @@ +mod play; +mod stop; +mod setavtransporturi; + +pub use play::PLAY; +pub use stop::STOP; +pub use setavtransporturi::SETAVTRANSPORTURI; + diff --git a/pmoupnp/src/mediarenderer/avtransport/actions/play.rs b/pmoupnp/src/mediarenderer/avtransport/actions/play.rs new file mode 100644 index 00000000..7f0b0e4d --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/actions/play.rs @@ -0,0 +1,19 @@ +use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, TRANSPORTPLAYSPEED}; +use crate::actions::{Action, Argument}; +use once_cell::sync::Lazy; + +pub static PLAY: Lazy = Lazy::new(|| -> Action { + let mut ac = Action::new("Play".to_string()); + + ac.add_argument( + Argument::new_in("InstanceID".to_string(), + A_ARG_TYPE_INSTANCE_ID.clone()) + ); + + ac.add_argument( + Argument::new_in("Speed".to_string(), + TRANSPORTPLAYSPEED.clone()) + ); + + ac +}); \ No newline at end of file diff --git a/pmoupnp/src/mediarenderer/avtransport/actions/setavtransporturi.rs b/pmoupnp/src/mediarenderer/avtransport/actions/setavtransporturi.rs new file mode 100644 index 00000000..a4825d36 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/actions/setavtransporturi.rs @@ -0,0 +1,10 @@ +use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, AVTRANSPORTURI, AVTRANSPORTURIMETADATA}; +use crate::define_action; + +define_action! { + pub static SETAVTRANSPORTURI = "SetAVTransportURI" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + in "CurrentURI" => AVTRANSPORTURI, + in "CurrentURIMetaData" => AVTRANSPORTURIMETADATA, + } +} diff --git a/pmoupnp/src/mediarenderer/avtransport/actions/stop.rs b/pmoupnp/src/mediarenderer/avtransport/actions/stop.rs new file mode 100644 index 00000000..c6a6fbd2 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/actions/stop.rs @@ -0,0 +1,8 @@ +use crate::mediarenderer::avtransport::variables::A_ARG_TYPE_INSTANCE_ID; +use crate::define_action; + +define_action! { + pub static STOP = "Stop" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + } +} diff --git a/pmoupnp/src/mediarenderer/avtransport/mod.rs b/pmoupnp/src/mediarenderer/avtransport/mod.rs new file mode 100644 index 00000000..86af9976 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/mod.rs @@ -0,0 +1,3 @@ +pub mod variables; +pub mod actions; + diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_instanceid.rs b/pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_instanceid.rs new file mode 100644 index 00000000..3c9990cb --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_instanceid.rs @@ -0,0 +1,7 @@ +use crate::state_variables::StateVariable; +use crate::variable_types::StateVarType; +use once_cell::sync::Lazy; + +pub static A_ARG_TYPE_INSTANCE_ID: Lazy = Lazy::new(|| -> StateVariable { + StateVariable::new(StateVarType::UI4, "A_ARG_TYPE_InstanceID".to_string()) +}); diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_playspeed.rs b/pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_playspeed.rs new file mode 100644 index 00000000..4e370d65 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_playspeed.rs @@ -0,0 +1,7 @@ +use crate::state_variables::StateVariable; +use crate::variable_types::StateVarType; +use once_cell::sync::Lazy; + +pub static A_ARG_TYPE_PLAY_SPEED: Lazy = Lazy::new(|| -> StateVariable { + StateVariable::new(StateVarType::String, "A_ARG_TYPE_PlaySpeed".to_string()) +}); diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/avtransporturi.rs b/pmoupnp/src/mediarenderer/avtransport/variables/avtransporturi.rs new file mode 100644 index 00000000..de6626b9 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/avtransporturi.rs @@ -0,0 +1,7 @@ +use crate::state_variables::StateVariable; +use crate::variable_types::StateVarType; +use once_cell::sync::Lazy; + +pub static AVTRANSPORTURI: Lazy = Lazy::new(|| -> StateVariable { + StateVariable::new(StateVarType::String, "AVTransportURI".to_string()) +}); diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/avtransporturimetadata.rs b/pmoupnp/src/mediarenderer/avtransport/variables/avtransporturimetadata.rs new file mode 100644 index 00000000..cfba4823 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/avtransporturimetadata.rs @@ -0,0 +1,33 @@ +use std::sync::Arc; + +use crate::state_variables::{StateVariable, StateVariableError}; +use crate::variable_types::StateVarType; +use bevy_reflect::Reflect; +use once_cell::sync::Lazy; +use pmodidl::{DIDLLite, MediaMetadataParser}; + +// func _AVTransportURIMetaDataParser(value string) (interface{}, error) { +// log.Debug("[avtransport] Parsing AVTransport)") +// didl, err := pmodidl.Parse(value) +// if err != nil { +// return value, err +// } + +// return didl, nil +// } + +fn avtransporturimetadataparser(value: &str) -> Result, StateVariableError> { + // Parse DIDL-Lite + let didl = DIDLLite::parse(value) + .map_err(|e| StateVariableError::ParseError(format!("Failed to parse DIDL-Lite: {}", e)))?; + + // Retourne le résultat sous forme de Box + Ok(Box::new(didl) as Box) +} + +pub static AVTRANSPORTURIMETADATA: Lazy = Lazy::new(|| -> StateVariable { + let mut sv = StateVariable::new(StateVarType::String, "AVTransportURIMetaData".to_string()); + + sv.set_value_parser(Arc::new(avtransporturimetadataparser)).expect("Failed to set parser"); + sv +}); diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/currenttrackduration.rs b/pmoupnp/src/mediarenderer/avtransport/variables/currenttrackduration.rs new file mode 100644 index 00000000..9bc15741 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/currenttrackduration.rs @@ -0,0 +1,8 @@ +use crate::state_variables::StateVariable; +use crate::variable_types::StateVarType; +use once_cell::sync::Lazy; + +pub static CURRENTTRACKDURATION: Lazy = Lazy::new(|| -> StateVariable { + StateVariable::new(StateVarType::String, "CurrentTrackDuration".to_string()) +}); + diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/mod.rs b/pmoupnp/src/mediarenderer/avtransport/variables/mod.rs new file mode 100644 index 00000000..73f08469 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/mod.rs @@ -0,0 +1,22 @@ +mod a_arg_type_instanceid; +mod a_arg_type_playspeed; +mod avtransporturi; +mod avtransporturimetadata; +mod currenttrackduration; +mod seekmode; +mod transportplayspeed; +mod transportstate; +mod transportstatus; + +pub use a_arg_type_instanceid::A_ARG_TYPE_INSTANCE_ID; +pub use a_arg_type_playspeed::A_ARG_TYPE_PLAY_SPEED; +pub use avtransporturi::AVTRANSPORTURI; +pub use avtransporturimetadata::AVTRANSPORTURIMETADATA; +pub use currenttrackduration::CURRENTTRACKDURATION; +pub use seekmode::SEEKMODE; +pub use transportplayspeed::TRANSPORTPLAYSPEED; +pub use transportstate::TRANSPORTSTATE; +pub use transportstatus::TRANSPORTSTATUS; + + + diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/seekmode.rs b/pmoupnp/src/mediarenderer/avtransport/variables/seekmode.rs new file mode 100644 index 00000000..aef97d40 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/seekmode.rs @@ -0,0 +1,8 @@ +use crate::state_variables::StateVariable; +use crate::variable_types::StateVarType; +use once_cell::sync::Lazy; + +pub static SEEKMODE: Lazy = Lazy::new(|| -> StateVariable { + StateVariable::new(StateVarType::String, "SeekMode".to_string()) +}); + diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/transportplayspeed.rs b/pmoupnp/src/mediarenderer/avtransport/variables/transportplayspeed.rs new file mode 100644 index 00000000..2b3e6da1 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/transportplayspeed.rs @@ -0,0 +1,13 @@ +use crate::state_variables::StateVariable; +use crate::variable_types::{StateValue, StateVarType}; +use once_cell::sync::Lazy; + +pub static TRANSPORTPLAYSPEED: Lazy = Lazy::new(|| -> StateVariable { + let mut sv = StateVariable::new(StateVarType::String, "TransportPlaySpeed".to_string()); + + sv.push_allowed_value(&StateValue::String("1".to_string())).expect("Cannot add allowed value"); + sv.set_default(&StateValue::String("1".to_string())).expect("Cannt set default value"); + + sv +}); + diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/transportstate.rs b/pmoupnp/src/mediarenderer/avtransport/variables/transportstate.rs new file mode 100644 index 00000000..12724c0f --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/transportstate.rs @@ -0,0 +1,21 @@ +use crate::state_variables::StateVariable; +use crate::variable_types::{StateValue, StateVarType}; +use once_cell::sync::Lazy; + +pub static TRANSPORTSTATE: Lazy = Lazy::new(|| -> StateVariable { + let mut sv = StateVariable::new(StateVarType::String, "TransportState".to_string()); + + sv.push_allowed_value(&StateValue::String("NO_MEDIA_PRESENT".to_string())).expect("Cannot add allowed value"); + sv.extend_allowed_values(&[ + StateValue::String("STOPPED".to_string()), + StateValue::String("PLAYING".to_string()), + StateValue::String("RECORDING".to_string()), + StateValue::String("TRANSITIONING".to_string()), + StateValue::String("PAUSED_PLAYBACK".to_string()), + StateValue::String("PAUSED_RECORDING".to_string()), + StateValue::String("NO_MEDIA_PRESENT".to_string()), + ]).expect("Cannt set default value"); + + sv +}); + diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/transportstatus.rs b/pmoupnp/src/mediarenderer/avtransport/variables/transportstatus.rs new file mode 100644 index 00000000..98d79338 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/transportstatus.rs @@ -0,0 +1,17 @@ +use crate::state_variables::StateVariable; +use crate::variable_types::{StateValue, StateVarType}; +use once_cell::sync::Lazy; + +pub static TRANSPORTSTATUS: Lazy = Lazy::new(|| -> StateVariable { + let mut sv = StateVariable::new(StateVarType::String, "TransportStatus".to_string()); + + sv.push_allowed_value(&StateValue::String("OK".to_string())) + .expect("Cannot add allowed value"); + sv.extend_allowed_values(&[ + StateValue::String("OK".to_string()), + StateValue::String("ERROR_OCCURRED".to_string()), + ]) + .expect("Cannt set default value"); + + sv +}); diff --git a/pmoupnp/src/mediarenderer/mod.rs b/pmoupnp/src/mediarenderer/mod.rs new file mode 100644 index 00000000..b9365d5a --- /dev/null +++ b/pmoupnp/src/mediarenderer/mod.rs @@ -0,0 +1 @@ +pub mod avtransport; diff --git a/pmoupnp/src/object_trait.rs b/pmoupnp/src/object_trait.rs index c18dea2a..4a8cd29a 100644 --- a/pmoupnp/src/object_trait.rs +++ b/pmoupnp/src/object_trait.rs @@ -24,8 +24,72 @@ pub trait UpnpXml { xml_string } + fn to_markdown(&self) -> String { + let elem = self.to_xml_element(); + let mut md = String::new(); + + fn is_url(s: &str) -> bool { + s.starts_with("http://") || s.starts_with("https://") || s.starts_with("urn:") + } + + fn is_image_url(s: &str) -> bool { + let s = s.to_lowercase(); + s.ends_with(".png") + || s.ends_with(".jpg") + || s.ends_with(".jpeg") + || s.ends_with(".gif") + || s.ends_with(".svg") + || s.ends_with(".webp") + } + + fn format_value(v: &str) -> String { + let v = v.trim().to_string(); // <-- clone du texte nettoyé + if is_url(&v) { + if is_image_url(&v) { + format!("[{}]({})
![]({})", v, v, v) + } else { + format!("[{}]({})", v, v) + } + } else { + format!("`{}`", v) + } + } + + fn recurse(elem: &xmltree::Element, md: &mut String, depth: usize) { + let indent = " ".repeat(depth); + md.push_str(&format!("{}- **{}**", indent, elem.name)); + + // Attributs + if !elem.attributes.is_empty() { + let attrs: Vec = elem + .attributes + .iter() + .map(|(k, v)| format!("{}={}", k, format_value(v))) + .collect(); + md.push_str(&format!(" ({})", attrs.join(", "))); + } + + // Texte (utilise get_text() maintenant) + if let Some(text) = elem.get_text().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()) { + md.push_str(&format!(": {}", format_value(&text))); + } + + md.push('\n'); + + // Enfants + for child in &elem.children { + if let xmltree::XMLNode::Element(child_elem) = child { + recurse(child_elem, md, depth + 1); + } + } + } + + md.push_str("# UPnP XML (Markdown view)\n\n"); + recurse(&elem, &mut md, 0); + md + } } -pub trait UpnpObject : UpnpXml { +pub trait UpnpObject: UpnpXml { fn as_upnp_object_type(&self) -> &UpnpObjectType; fn get_name(&self) -> &String { @@ -35,6 +99,4 @@ pub trait UpnpObject : UpnpXml { fn get_object_type(&self) -> &String { &self.as_upnp_object_type().object_type } - - } diff --git a/pmoupnp/src/server/mod.rs b/pmoupnp/src/server/mod.rs index 1ed5e125..303931ce 100644 --- a/pmoupnp/src/server/mod.rs +++ b/pmoupnp/src/server/mod.rs @@ -25,7 +25,7 @@ use rust_embed::RustEmbed; use serde::Serialize; use std::{net::SocketAddr, sync::Arc}; use tokio::{signal, sync::RwLock, task::JoinHandle}; -use tracing::{debug, error, info, warn}; +use tracing::{info, warn, debug, error}; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; @@ -150,16 +150,17 @@ impl Server { /// /// # Exemple /// - /// ```rust,no_run - /// # use pmoupnp::server::Server; - /// # use rust_embed::RustEmbed; + /// ```ignore + /// use pmoupnp::server::Server; + /// use rust_embed::RustEmbed; + /// /// #[derive(RustEmbed, Clone)] /// #[folder = "static/"] /// struct Assets; /// /// # #[tokio::main] /// # async fn main() { - /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); + /// let mut server = Server::new("Test", "http://localhost:3000", 3000); /// server.add_dir::("/assets").await; /// // Les fichiers de static/ sont accessibles via /assets/* /// # } @@ -169,9 +170,9 @@ impl Server { E: RustEmbed + Clone + Send + Sync + 'static, { let serve = ServeEmbed::::new(); - + let mut r = self.router.write().await; - + if path == "/" { *r = std::mem::take(&mut *r).fallback_service(serve); } else { @@ -229,9 +230,9 @@ impl Server { axum_embed::FallbackBehavior::Ok, Some("index.html".to_string()), ); - + let mut r = self.router.write().await; - + if path == "/" { *r = std::mem::take(&mut *r).fallback_service(serve); } else { @@ -288,17 +289,23 @@ impl Server { /// /// # Exemple avec SSE /// - /// ```rust,no_run - /// # use pmoupnp::server::Server; - /// # use axum::extract::State; - /// # use axum::response::sse::{Event, Sse, KeepAlive}; - /// # use tokio::sync::broadcast; - /// # #[derive(Clone)] - /// # struct LogState { tx: broadcast::Sender } - /// # impl LogState { fn subscribe(&self) -> broadcast::Receiver { self.tx.subscribe() } } - /// # #[tokio::main] - /// # async fn main() { - /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); + /// ```ignore + /// use pmoupnp::server::Server; + /// use axum::extract::State; + /// use axum::response::sse::{Event, Sse, KeepAlive}; + /// use tokio::sync::broadcast; + /// + /// #[derive(Clone)] + /// struct LogState { + /// tx: broadcast::Sender + /// } + /// + /// impl LogState { + /// fn subscribe(&self) -> broadcast::Receiver { + /// self.tx.subscribe() + /// } + /// } + /// /// async fn log_sse(State(state): State) -> Sse>> { /// let mut rx = state.subscribe(); /// let stream = async_stream::stream! { @@ -311,7 +318,6 @@ impl Server { /// /// let log_state = LogState { tx: broadcast::channel(100).0 }; /// server.add_handler_with_state("/logs", log_sse, log_state).await; - /// # } /// ``` pub async fn add_handler_with_state(&mut self, path: &str, handler: H, state: S) where @@ -319,7 +325,9 @@ impl Server { T: 'static, S: Clone + Send + Sync + 'static, { - let route = Router::new().route("/", get(handler)).with_state(state); + let route = Router::new() + .route("/", get(handler)) + .with_state(state); let mut r = self.router.write().await; *r = std::mem::take(&mut *r).nest(path, route); @@ -398,9 +406,16 @@ impl Server { /// /// # Exemple /// - /// ```rust,no_run + /// ```ignore /// use utoipa::OpenApi; /// use axum::{Router, Json, routing::get}; + /// use serde::{Serialize, Deserialize}; + /// + /// #[derive(Serialize, Deserialize, utoipa::ToSchema)] + /// struct User { + /// id: u64, + /// name: String, + /// } /// /// #[derive(utoipa::OpenApi)] /// #[openapi( @@ -429,7 +444,8 @@ impl Server { *api_r = Some(api_router); // Ajouter Swagger UI - let swagger = SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", openapi); + let swagger = SwaggerUi::new("/swagger-ui") + .url("/api-docs/openapi.json", openapi); let mut r = self.router.write().await; *r = std::mem::take(&mut *r).merge(swagger); @@ -453,10 +469,7 @@ impl Server { /// ``` pub async fn start(&mut self) { let addr = SocketAddr::from(([0, 0, 0, 0], self.http_port)); - info!( - "Server {} running at [http://{}:{}](http://{}:{})", - self.name, self.base_url, self.http_port, self.base_url, self.http_port - ); + info!("Server {} running at [http://{}:{}](http://{}:{})", self.name, self.base_url, self.http_port, self.base_url, self.http_port); // Merger le routeur API si présent let api_router = self.api_router.read().await; @@ -532,7 +545,7 @@ impl ServerBuilder { Self { name: "PMO-Music-Server".to_string(), base_url: config.get_base_url(), - http_port: config.get_http_port(), + http_port: config.get_http_port() } } @@ -550,4 +563,4 @@ impl ServerBuilder { pub fn build(self) -> Server { Server::new(self.name, self.base_url, self.http_port) } -} +} \ No newline at end of file diff --git a/pmoupnp/src/state_variables/mod.rs b/pmoupnp/src/state_variables/mod.rs index be971f7a..ff83479a 100644 --- a/pmoupnp/src/state_variables/mod.rs +++ b/pmoupnp/src/state_variables/mod.rs @@ -10,6 +10,7 @@ use std::{ }; pub use crate::state_variables::variable_trait::UpnpVariable; +use bevy_reflect::Reflect; use chrono::{DateTime, Utc}; pub use errors::StateVariableError; @@ -24,7 +25,7 @@ pub type StateConditionFunc = Arc bool + Send + Syn /// Type pour les fonctions de parsing de valeurs depuis des chaînes pub type StringValueParser = - Arc Result + Send + Sync>; + Arc Result, StateVariableError> + Send + Sync>; /// Type pour les fonctions de sérialisation de valeurs vers des chaînes pub type ValueSerializer = diff --git a/pmoupnp/src/state_variables/variable_methods.rs b/pmoupnp/src/state_variables/variable_methods.rs index 838709d5..54838466 100644 --- a/pmoupnp/src/state_variables/variable_methods.rs +++ b/pmoupnp/src/state_variables/variable_methods.rs @@ -295,7 +295,7 @@ impl StateVariable { self.description = description; } - pub fn set_default(&mut self, value: StateValue) -> Result<(), StateValueError> { + pub fn set_default(&mut self, value: &StateValue) -> Result<(), StateValueError> { if self.as_state_var_type() != value.as_state_var_type() { return Err(StateValueError::TypeError( "value does not have the right type".to_string(), @@ -353,7 +353,7 @@ impl StateVariable { self.send_events = false; } - pub fn set_value_parseur(&mut self, parser: StringValueParser) -> Result<(), StateValueError> { + pub fn set_value_parser(&mut self, parser: StringValueParser) -> Result<(), StateValueError> { if self.as_state_var_type() == StateVarType::String { self.parse = Some(parser); return Ok(()); diff --git a/pmoupnp/src/variable_types/mod.rs b/pmoupnp/src/variable_types/mod.rs index 0d6c22cf..f4144a76 100644 --- a/pmoupnp/src/variable_types/mod.rs +++ b/pmoupnp/src/variable_types/mod.rs @@ -30,6 +30,8 @@ mod values_from_uri; mod values_from_uuid; mod values_from_vec_u8; +mod values_from_str; + use std::fmt::Debug; use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime}; diff --git a/pmoupnp/src/variable_types/values_from_str.rs b/pmoupnp/src/variable_types/values_from_str.rs new file mode 100644 index 00000000..db8e6d3b --- /dev/null +++ b/pmoupnp/src/variable_types/values_from_str.rs @@ -0,0 +1,21 @@ +use std::convert::TryFrom; +use url::Url; + +use crate::variable_types::{StateValue, StateValueError}; + +impl TryFrom<&str> for StateValue { + type Error = StateValueError; + + fn try_from(s: &str) -> Result { + Ok(StateValue::String(s.to_string())) + } +} + +// Conversion depuis String +impl TryFrom for StateValue { + type Error = StateValueError; + + fn try_from(s: String) -> Result { + Ok(StateValue::String(s)) + } +} \ No newline at end of file diff --git a/pmoupnp/webapp/src/components/LogView.vue b/pmoupnp/webapp/src/components/LogView.vue index ead130e8..e24fdd8b 100644 --- a/pmoupnp/webapp/src/components/LogView.vue +++ b/pmoupnp/webapp/src/components/LogView.vue @@ -400,6 +400,7 @@ button.active { .message { color: #d4d4d4; word-break: break-word; + text-align: left; } @media (max-width: 768px) {