Començons un Mediarenderer

This commit is contained in:
2025-10-01 20:49:50 +02:00
parent 2da758aa24
commit 40c003cca4
32 changed files with 1485 additions and 42 deletions

353
Cargo.lock generated
View File

@@ -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"

View File

@@ -1,3 +1,3 @@
[workspace]
resolver = "3"
members = ["PMOMusic", "pmoupnp","pmoconfig", "pmoutils"]
members = ["PMOMusic", "pmoupnp","pmoconfig", "pmoutils", "pmodidl"]

View File

@@ -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;
}

12
pmodidl/Cargo.toml Normal file
View File

@@ -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"

580
pmodidl/src/lib.rs Normal file
View File

@@ -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<Self, Self::Error>;
/// 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<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")]
pub parsed_at: Option<std::time::SystemTime>,
}
impl<T> ParsedMetadata<T> {
pub fn new(format: impl Into<String>, 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<U, F>(self, f: F) -> ParsedMetadata<U>
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<P: MediaMetadataParser>(input: &str) -> Result<ParsedMetadata<P>, 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<Self, Self::Error> {
quick_xml::de::from_str(input)
}
fn format_name() -> &'static str {
"DIDL-Lite"
}
}
/// Type alias pour faciliter l'utilisation
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>,
}
/// 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<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>,
}
/// 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<String>,
#[serde(rename = "dc:title", alias = "title")]
pub title: String,
#[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")]
pub artist: Option<String>,
#[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")]
pub genre: Option<String>,
#[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")]
pub date: Option<String>,
#[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>,
}
/// 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<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,
}
/// 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<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>,
}
// ============= 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<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
F: Fn(&Container) -> bool,
{
self.all_containers().filter(move |c| predicate(c))
}
/// Filtre les items
pub fn filter_items<F>(&self, predicate: F) -> impl Iterator<Item = &Item>
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<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');
}
}
impl Item {
/// Itère sur les ressources audio uniquement
pub fn audio_resources(&self) -> impl Iterator<Item = &Resource> {
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()));
}
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::Item> {
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<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();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_didl() {
let xml = r#"
<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">
<item id="1" parentID="0">
<dc:title>Test Song</dc:title>
<upnp:class>object.item.audioItem.musicTrack</upnp:class>
<res protocolInfo="http-get:*:audio/mpeg:*">http://example.com/song.mp3</res>
</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)
let xml = r#"
<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">
<item id="1" parentID="0">
<title>Test Song</title>
<class>object.item.audioItem.musicTrack</class>
<res protocolInfo="http-get:*:audio/mpeg:*">http://example.com/song.mp3</res>
</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#"
<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
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#"
<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
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);
}
}

View File

@@ -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"

View File

@@ -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<Action> = 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,
}
}
}

View File

@@ -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,
}

View File

@@ -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)]

View File

@@ -0,0 +1,8 @@
mod play;
mod stop;
mod setavtransporturi;
pub use play::PLAY;
pub use stop::STOP;
pub use setavtransporturi::SETAVTRANSPORTURI;

View File

@@ -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<Action> = 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
});

View File

@@ -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,
}
}

View File

@@ -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,
}
}

View File

@@ -0,0 +1,3 @@
pub mod variables;
pub mod actions;

View File

@@ -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<StateVariable> = Lazy::new(|| -> StateVariable {
StateVariable::new(StateVarType::UI4, "A_ARG_TYPE_InstanceID".to_string())
});

View File

@@ -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<StateVariable> = Lazy::new(|| -> StateVariable {
StateVariable::new(StateVarType::String, "A_ARG_TYPE_PlaySpeed".to_string())
});

View File

@@ -0,0 +1,7 @@
use crate::state_variables::StateVariable;
use crate::variable_types::StateVarType;
use once_cell::sync::Lazy;
pub static AVTRANSPORTURI: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
StateVariable::new(StateVarType::String, "AVTransportURI".to_string())
});

View File

@@ -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<Box<dyn Reflect>, 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<dyn Reflect>
Ok(Box::new(didl) as Box<dyn Reflect>)
}
pub static AVTRANSPORTURIMETADATA: Lazy<StateVariable> = 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
});

View File

@@ -0,0 +1,8 @@
use crate::state_variables::StateVariable;
use crate::variable_types::StateVarType;
use once_cell::sync::Lazy;
pub static CURRENTTRACKDURATION: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
StateVariable::new(StateVarType::String, "CurrentTrackDuration".to_string())
});

View File

@@ -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;

View File

@@ -0,0 +1,8 @@
use crate::state_variables::StateVariable;
use crate::variable_types::StateVarType;
use once_cell::sync::Lazy;
pub static SEEKMODE: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
StateVariable::new(StateVarType::String, "SeekMode".to_string())
});

View File

@@ -0,0 +1,13 @@
use crate::state_variables::StateVariable;
use crate::variable_types::{StateValue, StateVarType};
use once_cell::sync::Lazy;
pub static TRANSPORTPLAYSPEED: Lazy<StateVariable> = 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
});

View File

@@ -0,0 +1,21 @@
use crate::state_variables::StateVariable;
use crate::variable_types::{StateValue, StateVarType};
use once_cell::sync::Lazy;
pub static TRANSPORTSTATE: Lazy<StateVariable> = 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
});

View File

@@ -0,0 +1,17 @@
use crate::state_variables::StateVariable;
use crate::variable_types::{StateValue, StateVarType};
use once_cell::sync::Lazy;
pub static TRANSPORTSTATUS: Lazy<StateVariable> = 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
});

View File

@@ -0,0 +1 @@
pub mod avtransport;

View File

@@ -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!("[{}]({})<br>![]({})", 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<String> = 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
}
}

View File

@@ -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>("/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::<E>::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<String> }
/// # impl LogState { fn subscribe(&self) -> broadcast::Receiver<String> { 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<String>
/// }
///
/// impl LogState {
/// fn subscribe(&self) -> broadcast::Receiver<String> {
/// self.tx.subscribe()
/// }
/// }
///
/// async fn log_sse(State(state): State<LogState>) -> Sse<impl futures::Stream<Item = Result<Event, std::convert::Infallible>>> {
/// 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<H, T, S>(&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)
}
}
}

View File

@@ -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<dyn Fn(&StateVarInstance) -> bool + Send + Syn
/// Type pour les fonctions de parsing de valeurs depuis des chaînes
pub type StringValueParser =
Arc<dyn Fn(&str) -> Result<StateValue, StateVariableError> + Send + Sync>;
Arc<dyn Fn(&str) -> Result<Box<dyn Reflect>, StateVariableError> + Send + Sync>;
/// Type pour les fonctions de sérialisation de valeurs vers des chaînes
pub type ValueSerializer =

View File

@@ -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(());

View File

@@ -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};

View File

@@ -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<Self, StateValueError> {
Ok(StateValue::String(s.to_string()))
}
}
// Conversion depuis String
impl TryFrom<String> for StateValue {
type Error = StateValueError;
fn try_from(s: String) -> Result<Self, StateValueError> {
Ok(StateValue::String(s))
}
}

View File

@@ -400,6 +400,7 @@ button.active {
.message {
color: #d4d4d4;
word-break: break-word;
text-align: left;
}
@media (max-width: 768px) {