diff --git a/.gitignore b/.gitignore index 7551fafb..5520f3bb 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ xxx **/.pmomusic_covers/** **/.DS_Strore/** **/.DS_Strore +/target/ .pmomusic_covers C/src/soxr-0.1.3/Release/tests **/Release/ @@ -20,3 +21,4 @@ OLD-GO-CODE/ xxx xx all.txt +pmo_src.txt diff --git a/.pmomusic.yml b/.pmomusic.yml index a7f84c7e..c91972b5 100644 --- a/.pmomusic.yml +++ b/.pmomusic.yml @@ -1,9 +1,9 @@ devices: - mediarenderer: - fakerenderer: - udn: d7eaad15-7d21-4411-926a-bc1eea0713db - mediaserver: - qobuz: - udn: 28963b75-4c5f-4da7-b10e-ffafd + mediarenderer: + fakerenderer: + udn: d7eaad15-7d21-4411-926a-bc1eea0713db + mediaserver: + qobuz: + udn: 28963b75-4c5f-4da7-b10e-ffafd host: - http_port: "8080" + http_port: '8080' diff --git a/.vscode/settings.json b/.vscode/settings.json index a227bbc0..2d7c8101 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,7 @@ { "makefile.configureOnOpen": false, - "git.enabled": false + "git.enabled": false, + "claude-code.environmentVariables": [ + + ] } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..2d2863b5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3044 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "PMOMusic" +version = "0.1.0" +dependencies = [ + "axum", + "pmoconfig", + "pmoupnp", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" +dependencies = [ + "axum-core 0.5.5", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-embed" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "077959a7f8cf438676af90b483304528eb7e16eadadb7f44e9ada4f9dceb9e62" +dependencies = [ + "axum-core 0.4.5", + "chrono", + "http", + "mime_guess", + "rust-embed", + "tower-service", +] + +[[package]] +name = "axum-server" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495c05f60d6df0093e8fb6e74aa5846a0ad06abaf96d76166283720bf740f8ab" +dependencies = [ + "bytes", + "fs-err", + "http", + "http-body", + "hyper", + "hyper-util", + "tokio", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link 0.2.0", +] + +[[package]] +name = "base64" +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" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "c_linked_list" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4964518bd3b4a8190e832886cdc0da9794f12e8e6c1613a9e90ff331c4c8724b" + +[[package]] +name = "cc" +version = "1.2.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.0", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.1", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "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 = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[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 = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.1", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "libz-rs-sys", + "miniz_oxide", +] + +[[package]] +name = "fnv" +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 = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-err" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f150ffc8782f35521cec2b23727707cb4045706ba3c854e86bef66b3a8cdbd" +dependencies = [ + "autocfg", + "tokio", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "get_if_addrs" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abddb55a898d32925f3148bd281174a68eeb68bbfd9a5938a57b18f506ee4ef7" +dependencies = [ + "c_linked_list", + "get_if_addrs-sys", + "libc", + "winapi", +] + +[[package]] +name = "get_if_addrs-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04f9fb746cf36b191c00f3ede8bde9c8e64f9f4b05ae2694a9ccf5e3f5ab48" +dependencies = [ + "gcc", + "libc", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.7+wasi-0.2.4", +] + +[[package]] +name = "gimli" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +dependencies = [ + "equivalent", + "serde", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown", + "serde", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.176" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "libz-rs-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" +dependencies = [ + "zlib-rs", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "pmoconfig" +version = "0.1.0" +dependencies = [ + "anyhow", + "dirs", + "lazy_static", + "log", + "pmoutils", + "serde", + "serde_yaml", + "tracing", + "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" +dependencies = [ + "anyhow", + "async-stream", + "axum", + "axum-embed", + "axum-server", + "base64", + "bevy_reflect", + "bevy_reflect_derive", + "chrono", + "futures", + "futures-util", + "get_if_addrs", + "hex", + "log", + "once_cell", + "parking_lot", + "pmoconfig", + "pmodidl", + "reqwest", + "rust-embed", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "tracing-subscriber", + "url", + "utoipa", + "utoipa-swagger-ui", + "uuid", + "validator", + "xmltree", +] + +[[package]] +name = "pmoutils" +version = "0.1.0" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rust-embed" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.1", +] + +[[package]] +name = "rustls" +version = "0.23.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.1", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.61.1", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utoipa" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "9.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55" +dependencies = [ + "axum", + "base64", + "mime_guess", + "regex", + "rust-embed", + "serde", + "serde_json", + "url", + "utoipa", + "zip", +] + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "validator" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" +dependencies = [ + "darling", + "once_cell", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "valuable" +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 = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.1", +] + +[[package]] +name = "windows-core" +version = "0.62.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.0", + "windows-result 0.4.0", + "windows-strings 0.5.0", +] + +[[package]] +name = "windows-implement" +version = "0.60.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +dependencies = [ + "windows-link 0.2.0", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +dependencies = [ + "windows-link 0.2.0", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +dependencies = [ + "windows-link 0.2.0", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "xml-rs" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" + +[[package]] +name = "xmltree" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b619f8c85654798007fb10afa5125590b43b088c225a25fc2fec100a9fad0fc6" +dependencies = [ + "xml-rs", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zip" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap", + "memchr", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..ea24bbb2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +resolver = "3" +members = ["PMOMusic", "pmoupnp","pmoconfig", "pmoutils", "pmodidl"] diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..6540c5c5 --- /dev/null +++ b/Makefile @@ -0,0 +1,190 @@ +# Makefile pour projet Rust + Vue.js +# Variables de configuration +CARGO = cargo +NPM = npm +WEBAPP_DIR = pmoupnp/webapp +DIST_DIR = $(WEBAPP_DIR)/dist +RUST_TARGET = target/release +DOC_DIR = target/doc +BINARY_NAME = PMOMusic + +# Couleurs pour l'affichage +GREEN = \033[0;32m +YELLOW = \033[1;33m +RED = \033[0;31m +NC = \033[0m # No Color + +.PHONY: all help build release debug test doc webapp clean install dev check fmt clippy watch + +# Cible par défaut +all: build + +## help: Affiche cette aide +help: + @echo "$(GREEN)Commandes disponibles :$(NC)" + @sed -n 's/^##//p' $(MAKEFILE_LIST) | column -t -s ':' | sed -e 's/^/ /' + +## build: Compile tout (Rust + Vue.js) +build: webapp release + @echo "$(GREEN)✓ Build complet terminé$(NC)" + +## release: Compile le binaire Rust en mode release +release: + @echo "$(YELLOW)→ Compilation Rust (release)...$(NC)" + $(CARGO) build --release + @echo "$(GREEN)✓ Binaire disponible : $(RUST_TARGET)/$(BINARY_NAME)$(NC)" + +## debug: Compile le binaire Rust en mode debug +debug: + @echo "$(YELLOW)→ Compilation Rust (debug)...$(NC)" + $(CARGO) build + @echo "$(GREEN)✓ Binaire disponible : target/debug/$(BINARY_NAME)$(NC)" + +## test: Exécute tous les tests Rust +test: + @echo "$(YELLOW)→ Exécution des tests Rust...$(NC)" + $(CARGO) test --all + @echo "$(GREEN)✓ Tests terminés$(NC)" + +## test-doc: Teste les exemples dans la documentation +test-doc: + @echo "$(YELLOW)→ Test des exemples de documentation...$(NC)" + $(CARGO) test --doc + @echo "$(GREEN)✓ Tests de documentation terminés$(NC)" + +## doc: Génère et ouvre la documentation Rust +doc: + @echo "$(YELLOW)→ Génération de la documentation...$(NC)" + $(CARGO) doc --no-deps --document-private-items --open + @echo "$(GREEN)✓ Documentation générée dans $(DOC_DIR)$(NC)" + +## doc-build: Génère la documentation sans l'ouvrir +doc-build: + @echo "$(YELLOW)→ Génération de la documentation...$(NC)" + $(CARGO) doc --no-deps --document-private-items + @echo "$(GREEN)✓ Documentation générée dans $(DOC_DIR)$(NC)" + +## webapp: Compile l'application Vue.js +webapp: webapp-install + @echo "$(YELLOW)→ Build Vue.js...$(NC)" + cd $(WEBAPP_DIR) && $(NPM) run build + @echo "$(GREEN)✓ Application Vue.js compilée dans $(DIST_DIR)$(NC)" + +## webapp-install: Installe les dépendances npm +webapp-install: + @echo "$(YELLOW)→ Installation des dépendances npm...$(NC)" + cd $(WEBAPP_DIR) && $(NPM) install + @echo "$(GREEN)✓ Dépendances npm installées$(NC)" + +## webapp-dev: Lance le serveur de développement Vue.js +webapp-dev: + @echo "$(YELLOW)→ Démarrage du serveur de dev Vue.js...$(NC)" + cd $(WEBAPP_DIR) && $(NPM) run dev + +## clean: Nettoie les fichiers de build +clean: + @echo "$(YELLOW)→ Nettoyage...$(NC)" + $(CARGO) clean + rm -rf $(DIST_DIR) + rm -rf $(WEBAPP_DIR)/node_modules + @echo "$(GREEN)✓ Nettoyage terminé$(NC)" + +## clean-rust: Nettoie uniquement les builds Rust +clean-rust: + @echo "$(YELLOW)→ Nettoyage Rust...$(NC)" + $(CARGO) clean + @echo "$(GREEN)✓ Nettoyage Rust terminé$(NC)" + +## clean-webapp: Nettoie uniquement le build Vue.js +clean-webapp: + @echo "$(YELLOW)→ Nettoyage Vue.js...$(NC)" + rm -rf $(DIST_DIR) + @echo "$(GREEN)✓ Nettoyage Vue.js terminé$(NC)" + +## install: Installe le binaire dans ~/.cargo/bin +install: release + @echo "$(YELLOW)→ Installation du binaire...$(NC)" + $(CARGO) install --path . + @echo "$(GREEN)✓ $(BINARY_NAME) installé$(NC)" + +## dev: Lance le serveur en mode debug (recompile à chaque changement) +dev: + @echo "$(YELLOW)→ Démarrage en mode développement...$(NC)" + $(CARGO) watch -x run + +## check: Vérifie que le code compile sans générer de binaire +check: + @echo "$(YELLOW)→ Vérification du code...$(NC)" + $(CARGO) check --all + @echo "$(GREEN)✓ Code valide$(NC)" + +## fmt: Formate le code Rust +fmt: + @echo "$(YELLOW)→ Formatage du code...$(NC)" + $(CARGO) fmt --all + @echo "$(GREEN)✓ Code formaté$(NC)" + +## fmt-check: Vérifie le formatage sans modifier +fmt-check: + @echo "$(YELLOW)→ Vérification du formatage...$(NC)" + $(CARGO) fmt --all -- --check + +## clippy: Exécute clippy (linter Rust) +clippy: + @echo "$(YELLOW)→ Analyse avec clippy...$(NC)" + $(CARGO) clippy --all-targets --all-features -- -D warnings + @echo "$(GREEN)✓ Analyse clippy terminée$(NC)" + +## watch: Recompile automatiquement à chaque changement +watch: + @echo "$(YELLOW)→ Mode watch activé...$(NC)" + $(CARGO) watch -x check + +## ci: Exécute toutes les vérifications CI +ci: fmt-check clippy test doc-build webapp + @echo "$(GREEN)✓ Toutes les vérifications CI passées$(NC)" + +## run: Lance le binaire en mode debug +run: debug + @echo "$(YELLOW)→ Lancement de l'application...$(NC)" + ./target/debug/$(BINARY_NAME) + +## run-release: Lance le binaire en mode release +run-release: release + @echo "$(YELLOW)→ Lancement de l'application (release)...$(NC)" + ./$(RUST_TARGET)/$(BINARY_NAME) + +## size: Affiche la taille du binaire +size: + @echo "$(YELLOW)Taille des binaires :$(NC)" + @if [ -f "target/debug/$(BINARY_NAME)" ]; then \ + echo " Debug: $$(du -h target/debug/$(BINARY_NAME) | cut -f1)"; \ + fi + @if [ -f "$(RUST_TARGET)/$(BINARY_NAME)" ]; then \ + echo " Release: $$(du -h $(RUST_TARGET)/$(BINARY_NAME) | cut -f1)"; \ + fi + +## deps: Liste les dépendances obsolètes +deps: + @echo "$(YELLOW)→ Vérification des dépendances...$(NC)" + $(CARGO) outdated + +## update: Met à jour les dépendances +update: + @echo "$(YELLOW)→ Mise à jour des dépendances Rust...$(NC)" + $(CARGO) update + @echo "$(YELLOW)→ Mise à jour des dépendances npm...$(NC)" + cd $(WEBAPP_DIR) && $(NPM) update + @echo "$(GREEN)✓ Dépendances mises à jour$(NC)" + +## bench: Exécute les benchmarks +bench: + @echo "$(YELLOW)→ Exécution des benchmarks...$(NC)" + $(CARGO) bench + +## coverage: Génère un rapport de couverture de code +coverage: + @echo "$(YELLOW)→ Génération du rapport de couverture...$(NC)" + $(CARGO) tarpaulin --out Html --output-dir target/coverage + @echo "$(GREEN)✓ Rapport disponible dans target/coverage/index.html$(NC)" + \ No newline at end of file diff --git a/PMOMusic/Cargo.toml b/PMOMusic/Cargo.toml new file mode 100644 index 00000000..05a06ed5 --- /dev/null +++ b/PMOMusic/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "PMOMusic" +version = "0.1.0" +edition = "2024" + +[dependencies] +pmoconfig = { path = "../pmoconfig" } +pmoupnp = { path = "../pmoupnp"} + + +tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time","signal"] } +tracing = "0.1.41" +tracing-subscriber = "0.3.20" +axum = "0.8.4" +serde_json = "1.0.145" diff --git a/PMOMusic/src/main.rs b/PMOMusic/src/main.rs new file mode 100644 index 00000000..f0d7a29b --- /dev/null +++ b/PMOMusic/src/main.rs @@ -0,0 +1,55 @@ +use pmoupnp::{mediarenderer::avtransport::{actions::SETAVTRANSPORTURI, AVTTRANSPORT}, server::{ + logs::{log_dump, log_sse, LogState, SseLayer}, ServerBuilder, Webapp +}, UpnpObject}; // ton module pmoupnp::server +use tracing_subscriber::Registry; +use tracing_subscriber::prelude::*; +use tracing::info; + +#[tokio::main] +async fn main() { + // Charger la config + + let mut server = ServerBuilder::new_configured().build(); + + // Ajouter des routes + server + .add_route("/hello", || async { + serde_json::json!({"message": "Hello World"}) + }) + .await; + + server + .add_route("/info", || async { + serde_json::json!({"version": "1.0.0"}) + }) + .await; + + server.add_spa::("/app").await; + + // Gère la sortie des logs et sur le serveur SSE pour l'interface web et sur la console + let log_state = LogState::new(1000); + let subscriber = Registry::default() + .with( + tracing_subscriber::fmt::layer() + .with_target(true) + .with_level(true) + .with_ansi(true), // Couleurs dans le terminal + ) + .with(SseLayer::new(log_state.clone())); + tracing::subscriber::set_global_default(subscriber).unwrap(); + + server + .add_handler_with_state("/log-sse", log_sse, log_state.clone()) + .await; + server + .add_handler_with_state("/log-dump", log_dump, log_state.clone()) + .await; + + server.add_redirect("/", "/app").await; + + info!("{}",AVTTRANSPORT.to_markdown()); + info!("{}",AVTTRANSPORT.scpd_xml()); + + server.start().await; + server.wait().await; +} diff --git a/README.md b/README.md deleted file mode 100644 index bd5a3eb5..00000000 --- a/README.md +++ /dev/null @@ -1,9 +0,0 @@ - -## Installing documentation system Doc-Gen - -https://github.com/fynnfluegge/doc-comments-ai - - -for f in upnp/*.go ; do - dcai/bin/aicomment --ollama-model deepseek-coder:33b-instruct $f -done \ No newline at end of file diff --git a/Readme.md b/Readme.md new file mode 100644 index 00000000..70265fdf --- /dev/null +++ b/Readme.md @@ -0,0 +1,100 @@ +# Développement de l'application PMOMusic en RUST + +## Création de la structure + +```bash +mkdir pizzicato +cd pizzicato +jj git init +touch Readme.md +``` + +Maintenant on peu créer l'application PMOMusic + +```bash +cargo new PMOMusic +``` + +On ajoute un fichier `Cargo.toml` décrivant le workspace pizzicato qui ne contient que notre nouvelle application + +``` +[workspace] +members = ["PMOMusic"] +``` + +On crée le package `pmoupnp` + +```bash +cargo new pmoupnp --lib +``` + +Cela modifie automatiquement le fichier `Cargo.toml` créé juste avant. +Dans ce package un sous module `statevariable` + +```bash +cd pmoupnp/src +mkdir statevariable +``` + + +# Petite expériences jujutsu + +- Je veux voir l'historique + +```bash +jj log +```` +``` +@ lkmlpmnk eric@coissac.eu 2025-09-12 09:48:38 a27b6a9a +│ On commence les states variables +○ mzvokmpk eric@coissac.eu 2025-09-12 09:32:27 926827f3 +│ Retire le répertoir target du suivi +○ skknrvut eric@coissac.eu 2025-09-12 09:05:27 cbad5c34 +│ Initialisation des l'arborescence de répertoires +◆ zzzzzzzz root() 00000000 +``` + +- Je veux me mettre dans un commit: + +```bash +jj edit mzvokmpk +``` + +je veux créer un nouveau commit à la suite d'un autre et m'y placer + +```bash +jj new mzvokmpk +``` + +`mzvokmpk` peut être `@` pour dans le commit courant ou `@-` pour dans le parent + +- je veux arreter de suivre + - un fichier + +```bash +jj file untrack +``` + + - un répertoire + +```bash +find dirname -type f -exec jj file untrack {} \; +``` + +Dans tous les cas ne pas oublier d'inscrire le fichier ou le repertoire dans le `.gitignore` + +- je veux déplacer un commit comme un sous commit d'un aute + +```bash +jj rebase -d destination -r source +``` + +eventuellement faire un + +```bash +jj resolve --all +jj rebase --continue +``` + +pour résoudre les conflits + diff --git a/deps.svg b/deps.svg new file mode 100644 index 00000000..fb45d05d --- /dev/null +++ b/deps.svg @@ -0,0 +1,3168 @@ + + + + + + + + + +0 + +PMOMusic + + + +1 + +pmoconfig + + + +0->1 + + + + + +3 + +pmoupnp + + + +0->3 + + + + + +5 + +axum + + + +0->5 + + + + + +6 + +serde_json + + + +0->6 + + + + + +7 + +tokio + + + +0->7 + + + + + +8 + +tracing + + + +0->8 + + + + + +9 + +tracing-subscriber + + + +0->9 + + + + + +2 + +pmoutils + + + +1->2 + + + + + +1->8 + + + + + +10 + +anyhow + + + +1->10 + + + + + +11 + +dirs + + + +1->11 + + + + + +12 + +lazy_static + + + +1->12 + + + + + +13 + +log + + + +1->13 + + + + + +14 + +serde + + + +1->14 + + + + + +15 + +serde_yaml + + + +1->15 + + + + + +16 + +uuid + + + +1->16 + + + + + +17 + +get_if_addrs + + + +2->17 + + + + + +3->1 + + + + + +4 + +pmodidl + + + +3->4 + + + + + +3->5 + + + + + +3->6 + + + + + +3->7 + + + + + +3->8 + + + + + +3->9 + + + + + +3->10 + + + + + +3->13 + + + + + +3->14 + + + + + +3->16 + + + + + +3->17 + + + + + +18 + +async-stream + + + +3->18 + + + + + +19 + +axum-embed + + + +3->19 + + + + + +20 + +axum-server + + + +3->20 + + + + + +21 + +base64 + + + +3->21 + + + + + +22 + +bevy_reflect + + + +3->22 + + + + + +23 + +chrono + + + +3->23 + + + + + +24 + +futures + + + +3->24 + + + + + +25 + +futures-util + + + +3->25 + + + + + +26 + +hex + + + +3->26 + + + + + +27 + +once_cell + + + +3->27 + + + + + +28 + +parking_lot + + + +3->28 + + + + + +29 + +rust-embed + + + +3->29 + + + + + +30 + +thiserror + + + +3->30 + + + + + +31 + +tokio-stream + + + +3->31 + + + + + +32 + +url + + + +3->32 + + + + + +33 + +utoipa + + + +3->33 + + + + + +34 + +utoipa-swagger-ui + + + +3->34 + + + + + +35 + +validator + + + +3->35 + + + + + +36 + +xmltree + + + +3->36 + + + + + +4->14 + + + + + +4->22 + + + + + +4->33 + + + + + +4->34 + + + + + +37 + +quick-xml + + + +4->37 + + + + + +5->6 + + + + + +5->7 + + + + + +5->8 + + + + + +5->25 + + + + + +38 + +axum-core 0.5.5 + + + +5->38 + + + + + +39 + +bytes + + + +5->39 + + + + + +40 + +form_urlencoded + + + +5->40 + + + + + +41 + +http + + + +5->41 + + + + + +42 + +http-body + + + +5->42 + + + + + +43 + +http-body-util + + + +5->43 + + + + + +44 + +hyper + + + +5->44 + + + + + +45 + +hyper-util + + + +5->45 + + + + + +46 + +itoa + + + +5->46 + + + + + +47 + +matchit + + + +5->47 + + + + + +48 + +memchr + + + +5->48 + + + + + +49 + +mime + + + +5->49 + + + + + +50 + +percent-encoding + + + +5->50 + + + + + +51 + +pin-project-lite + + + +5->51 + + + + + +52 + +serde_core + + + +5->52 + + + + + +53 + +serde_path_to_error + + + +5->53 + + + + + +54 + +serde_urlencoded + + + +5->54 + + + + + +55 + +sync_wrapper + + + +5->55 + + + + + +56 + +tower + + + +5->56 + + + + + +57 + +tower-layer + + + +5->57 + + + + + +58 + +tower-service + + + +5->58 + + + + + +6->46 + + + + + +6->48 + + + + + +6->52 + + + + + +59 + +ryu + + + +6->59 + + + + + +7->39 + + + + + +7->51 + + + + + +60 + +mio + + + +7->60 + + + + + +8->13 + + + + + +8->51 + + + + + +61 + +tracing-core + + + +8->61 + + + + + +9->8 + + + + + +9->27 + + + + + +9->61 + + + + + +62 + +matchers + + + +9->62 + + + + + +63 + +nu-ansi-term + + + +9->63 + + + + + +64 + +regex-automata + + + +9->64 + + + + + +65 + +sharded-slab + + + +9->65 + + + + + +66 + +smallvec + + + +9->66 + + + + + +67 + +thread_local + + + +9->67 + + + + + +68 + +tracing-log + + + +9->68 + + + + + +69 + +dirs-sys + + + +11->69 + + + + + +14->52 + + + + + +15->14 + + + + + +15->46 + + + + + +15->59 + + + + + +70 + +indexmap + + + +15->70 + + + + + +71 + +unsafe-libyaml + + + +15->71 + + + + + +16->14 + + + + + +72 + +c_linked_list + + + +17->72 + + + + + +73 + +libc + + + +17->73 + + + + + +18->51 + + + + + +74 + +futures-core + + + +18->74 + + + + + +19->23 + + + + + +19->29 + + + + + +19->41 + + + + + +19->58 + + + + + +75 + +axum-core 0.4.5 + + + +19->75 + + + + + +76 + +mime_guess + + + +19->76 + + + + + +20->7 + + + + + +20->39 + + + + + +20->41 + + + + + +20->42 + + + + + +20->44 + + + + + +20->45 + + + + + +20->58 + + + + + +77 + +fs-err + + + +20->77 + + + + + +22->14 + + + + + +22->16 + + + + + +22->30 + + + + + +22->66 + + + + + +78 + +bevy_platform + + + +22->78 + + + + + +79 + +bevy_ptr + + + +22->79 + + + + + +80 + +bevy_utils + + + +22->80 + + + + + +81 + +derive_more + + + +22->81 + + + + + +82 + +disqualified + + + +22->82 + + + + + +83 + +downcast-rs + + + +22->83 + + + + + +84 + +erased-serde + + + +22->84 + + + + + +85 + +foldhash + + + +22->85 + + + + + +86 + +glam + + + +22->86 + + + + + +87 + +inventory + + + +22->87 + + + + + +88 + +smol_str + + + +22->88 + + + + + +89 + +wgpu-types + + + +22->89 + + + + + +23->14 + + + + + +90 + +num-traits + + + +23->90 + + + + + +24->25 + + + + + +24->74 + + + + + +91 + +futures-channel + + + +24->91 + + + + + +92 + +futures-executor + + + +24->92 + + + + + +93 + +futures-io + + + +24->93 + + + + + +94 + +futures-sink + + + +24->94 + + + + + +95 + +futures-task + + + +24->95 + + + + + +25->48 + + + + + +25->51 + + + + + +25->74 + + + + + +25->91 + + + + + +25->93 + + + + + +25->94 + + + + + +25->95 + + + + + +96 + +pin-utils + + + +25->96 + + + + + +97 + +slab + + + +25->97 + + + + + +98 + +lock_api + + + +28->98 + + + + + +99 + +parking_lot_core + + + +28->99 + + + + + +100 + +rust-embed-utils + + + +29->100 + + + + + +101 + +walkdir + + + +29->101 + + + + + +31->7 + + + + + +31->51 + + + + + +31->74 + + + + + +32->14 + + + + + +32->40 + + + + + +32->50 + + + + + +102 + +idna + + + +32->102 + + + + + +33->6 + + + + + +33->14 + + + + + +33->70 + + + + + +34->5 + + + + + +34->6 + + + + + +34->14 + + + + + +34->21 + + + + + +34->29 + + + + + +34->33 + + + + + +34->76 + + + + + +35->6 + + + + + +35->14 + + + + + +35->27 + + + + + +35->32 + + + + + +35->102 + + + + + +103 + +regex + + + +35->103 + + + + + +104 + +xml-rs + + + +36->104 + + + + + +37->14 + + + + + +37->48 + + + + + +38->8 + + + + + +38->39 + + + + + +38->41 + + + + + +38->42 + + + + + +38->43 + + + + + +38->49 + + + + + +38->51 + + + + + +38->55 + + + + + +38->57 + + + + + +38->58 + + + + + +38->74 + + + + + +40->50 + + + + + +41->39 + + + + + +41->46 + + + + + +105 + +fnv + + + +41->105 + + + + + +42->39 + + + + + +42->41 + + + + + +43->39 + + + + + +43->41 + + + + + +43->42 + + + + + +43->51 + + + + + +43->74 + + + + + +44->7 + + + + + +44->39 + + + + + +44->41 + + + + + +44->42 + + + + + +44->46 + + + + + +44->51 + + + + + +44->66 + + + + + +44->74 + + + + + +44->91 + + + + + +44->96 + + + + + +106 + +atomic-waker + + + +44->106 + + + + + +107 + +h2 + + + +44->107 + + + + + +108 + +httparse + + + +44->108 + + + + + +109 + +httpdate + + + +44->109 + + + + + +45->7 + + + + + +45->39 + + + + + +45->41 + + + + + +45->42 + + + + + +45->44 + + + + + +45->51 + + + + + +45->58 + + + + + +45->74 + + + + + +53->46 + + + + + +53->52 + + + + + +54->14 + + + + + +54->40 + + + + + +54->46 + + + + + +54->59 + + + + + +56->7 + + + + + +56->8 + + + + + +56->25 + + + + + +56->51 + + + + + +56->55 + + + + + +56->57 + + + + + +56->58 + + + + + +56->74 + + + + + +61->27 + + + + + +62->64 + + + + + +64->48 + + + + + +110 + +aho-corasick + + + +64->110 + + + + + +111 + +regex-syntax + + + +64->111 + + + + + +65->12 + + + + + +112 + +cfg-if + + + +67->112 + + + + + +68->13 + + + + + +68->27 + + + + + +68->61 + + + + + +113 + +option-ext + + + +69->113 + + + + + +70->52 + + + + + +114 + +equivalent + + + +70->114 + + + + + +115 + +hashbrown + + + +70->115 + + + + + +75->25 + + + + + +75->39 + + + + + +75->41 + + + + + +75->42 + + + + + +75->43 + + + + + +75->49 + + + + + +75->51 + + + + + +75->55 + + + + + +75->57 + + + + + +75->58 + + + + + +76->49 + + + + + +116 + +unicase + + + +76->116 + + + + + +77->7 + + + + + +78->14 + + + + + +78->85 + + + + + +78->115 + + + + + +117 + +critical-section + + + +78->117 + + + + + +118 + +spin + + + +78->118 + + + + + +80->78 + + + + + +80->82 + + + + + +84->52 + + + + + +119 + +typeid + + + +84->119 + + + + + +86->52 + + + + + +88->14 + + + + + +89->13 + + + + + +89->14 + + + + + +89->30 + + + + + +120 + +bitflags + + + +89->120 + + + + + +121 + +bytemuck + + + +89->121 + + + + + +91->74 + + + + + +91->94 + + + + + +92->25 + + + + + +92->74 + + + + + +92->95 + + + + + +122 + +scopeguard + + + +98->122 + + + + + +99->66 + + + + + +99->112 + + + + + +100->101 + + + + + +123 + +sha2 + + + +100->123 + + + + + +124 + +same-file + + + +101->124 + + + + + +102->66 + + + + + +125 + +idna_adapter + + + +102->125 + + + + + +126 + +utf8_iter + + + +102->126 + + + + + +103->48 + + + + + +103->64 + + + + + +103->110 + + + + + +103->111 + + + + + +107->7 + + + + + +107->8 + + + + + +107->39 + + + + + +107->41 + + + + + +107->70 + + + + + +107->74 + + + + + +107->94 + + + + + +107->97 + + + + + +107->105 + + + + + +107->106 + + + + + +127 + +tokio-util + + + +107->127 + + + + + +110->48 + + + + + +115->14 + + + + + +115->114 + + + + + +128 + +portable-atomic + + + +118->128 + + + + + +120->14 + + + + + +123->112 + + + + + +129 + +digest + + + +123->129 + + + + + +130 + +icu_normalizer + + + +125->130 + + + + + +131 + +icu_properties + + + +125->131 + + + + + +127->7 + + + + + +127->39 + + + + + +127->51 + + + + + +127->74 + + + + + +127->94 + + + + + +132 + +block-buffer + + + +129->132 + + + + + +133 + +crypto-common + + + +129->133 + + + + + +130->66 + + + + + +130->131 + + + + + +134 + +icu_collections + + + +130->134 + + + + + +135 + +icu_normalizer_data + + + +130->135 + + + + + +136 + +icu_provider + + + +130->136 + + + + + +137 + +zerovec + + + +130->137 + + + + + +131->134 + + + + + +131->136 + + + + + +131->137 + + + + + +138 + +icu_locale_core + + + +131->138 + + + + + +139 + +icu_properties_data + + + +131->139 + + + + + +140 + +potential_utf + + + +131->140 + + + + + +141 + +zerotrie + + + +131->141 + + + + + +142 + +generic-array + + + +132->142 + + + + + +133->142 + + + + + +143 + +typenum + + + +133->143 + + + + + +134->137 + + + + + +134->140 + + + + + +144 + +yoke + + + +134->144 + + + + + +145 + +zerofrom + + + +134->145 + + + + + +136->137 + + + + + +136->138 + + + + + +136->141 + + + + + +136->144 + + + + + +136->145 + + + + + +146 + +stable_deref_trait + + + +136->146 + + + + + +147 + +tinystr + + + +136->147 + + + + + +148 + +writeable + + + +136->148 + + + + + +137->144 + + + + + +137->145 + + + + + +138->137 + + + + + +138->147 + + + + + +138->148 + + + + + +149 + +litemap + + + +138->149 + + + + + +140->137 + + + + + +141->144 + + + + + +141->145 + + + + + +142->143 + + + + + +144->14 + + + + + +144->145 + + + + + +144->146 + + + + + +147->137 + + + + + diff --git a/doc/traits_object.md b/doc/traits_object.md new file mode 100644 index 00000000..3b6ce8a4 --- /dev/null +++ b/doc/traits_object.md @@ -0,0 +1,46 @@ +```mermaid +graph TB + Clone[Clone
std trait]:::stdTrait + Debug[Debug
std trait]:::stdTrait + + UpnpDeepClone[UpnpDeepClone
deep_clone]:::baseTrait + + UpnpObject[UpnpObject
to_xml_element
to_xml
to_markdown]:::baseTrait + + UpnpModel[UpnpModel
create_instance]:::derived1 + UpnpInstance[UpnpInstance
new]:::derived1 + UpnpTyped[UpnpTyped
get_name
get_object_type]:::derived1 + UpnpSet[UpnpSet
is_set]:::derived1 + + UpnpTypedObject[UpnpTypedObject
marker]:::derived2 + + UpnpTypedInstance[UpnpTypedInstance
marker]:::derived3 + UpnpModelSet[UpnpModelSet
marker]:::derived3 + UpnInstanceSet[UpnInstanceSet
marker]:::derived3 + + Clone --> UpnpObject + Debug --> UpnpObject + + UpnpObject --> UpnpModel + UpnpObject --> UpnpInstance + UpnpObject --> UpnpTyped + UpnpObject --> UpnpSet + + UpnpObject --> UpnpTypedObject + UpnpTyped --> UpnpTypedObject + + UpnpTypedObject --> UpnpTypedInstance + UpnpInstance --> UpnpTypedInstance + + UpnpSet --> UpnpModelSet + UpnpModel --> UpnpModelSet + + UpnpSet --> UpnInstanceSet + UpnpInstance --> UpnInstanceSet + + classDef stdTrait fill:#e1f5ff,stroke:#01579b,stroke-width:2px + classDef baseTrait fill:#fff3e0,stroke:#e65100,stroke-width:2px + classDef derived1 fill:#f3e5f5,stroke:#4a148c,stroke-width:2px + classDef derived2 fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px + classDef derived3 fill:#fce4ec,stroke:#880e4f,stroke-width:2px +``` \ No newline at end of file diff --git a/pmoconfig/.gitignore b/pmoconfig/.gitignore new file mode 100644 index 00000000..ea8c4bf7 --- /dev/null +++ b/pmoconfig/.gitignore @@ -0,0 +1 @@ +/target diff --git a/pmoconfig/Cargo.toml b/pmoconfig/Cargo.toml new file mode 100644 index 00000000..f42a2581 --- /dev/null +++ b/pmoconfig/Cargo.toml @@ -0,0 +1,17 @@ + +[package] +name = "pmoconfig" +version = "0.1.0" +edition = "2021" + +[dependencies] +pmoutils ={ path = "../pmoutils" } + +serde = { version = "1.0", features = ["derive"] } +serde_yaml = "0.9.33" +lazy_static = "1.4.0" +dirs = "6.0.0" +log = "0.4.20" +anyhow = "1.0.75" +uuid = { version = "1.18.1", features = ["v4"] } +tracing = "0.1.41" \ No newline at end of file diff --git a/pmoconfig/src/lib.rs b/pmoconfig/src/lib.rs new file mode 100644 index 00000000..580ed071 --- /dev/null +++ b/pmoconfig/src/lib.rs @@ -0,0 +1,319 @@ +use anyhow::{anyhow, Result}; +use dirs::home_dir; +use lazy_static::lazy_static; +use pmoutils::guess_local_ip; +use serde_yaml::{Mapping, Value}; +use std::{ + env, fs, + path::{Path, PathBuf}, + sync::{Arc, Mutex}, +}; +use tracing::{info, warn}; +use uuid::Uuid; + +// Configuration par défaut intégrée +const DEFAULT_CONFIG: &str = include_str!("pmomusic.yaml"); + +lazy_static! { + static ref CONFIG: Arc = + Arc::new(Config::load_config("").expect("Failed to load PMOMusic configuration")); +} + +const ENV_CONFIG_FILE: &str = "PMOMUSIC_CONFIG"; +const ENV_PREFIX: &str = "PMOMUSIC_CONFIG__"; + +#[derive(Debug)] +pub struct Config { + path: String, + data: Mutex, +} + +// Implémentation manuelle de Clone +impl Clone for Config { + fn clone(&self) -> Self { + let data = self.data.lock().unwrap().clone(); + Self { + path: self.path.clone(), + data: Mutex::new(data), + } + } +} + +impl Config { + pub fn load_config(filename: &str) -> Result { + let mut path = filename.to_string(); + let mut data: Option> = None; + + // Essayer de charger depuis différents emplacements + if !filename.is_empty() { + info!(config_file=%path, "Trying to load config"); + data = fs::read(&path).ok(); + if data.is_none() { + warn!(config_file=%path, "Cannot read config file"); + path.clear(); + } + } + + if path.is_empty() { + if let Ok(env_path) = env::var(ENV_CONFIG_FILE) { + info!(env_var=ENV_CONFIG_FILE, path=%env_path, "Trying to load config from env"); + path = env_path.clone(); + data = fs::read(&path).ok(); + if data.is_none() { + warn!(config_file=%path, "Cannot read config file from env var"); + path.clear(); + } + } + } + + if path.is_empty() { + let current_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + path = current_dir + .join(".pmomusic.yml") + .to_string_lossy() + .to_string(); + info!(config_file=%path, "Trying to load config file from current directory"); + data = fs::read(&path).ok(); + if data.is_none() { + warn!(config_file=%path, "Cannot read config file in current dir"); + path.clear(); + } + } + + if path.is_empty() { + path = Self::get_home_yml_path(); + info!(config_file=%path, "Trying to load config file from home directory"); + data = fs::read(&path).ok(); + if data.is_none() { + warn!(config_file=%path, "Cannot read config file in home directory"); + path.clear(); + } + } + + let yaml_data = if let Some(d) = data { + d + } else { + info!("Using default embedded config"); + DEFAULT_CONFIG.as_bytes().to_vec() + }; + + let mut config_value: Value = serde_yaml::from_slice(&yaml_data)?; + config_value = Self::lower_keys_value(config_value); + Self::apply_env_overrides(&mut config_value); + + if path.is_empty() || !Self::is_writable(&path) { + let candidates = [ + filename.to_string(), + env::var(ENV_CONFIG_FILE).unwrap_or_default(), + ".pmomusic.yml".to_string(), + Self::get_home_yml_path(), + ]; + for candidate in candidates.iter().filter(|c| !c.is_empty()) { + if Self::is_writable(candidate) { + path = candidate.clone(); + break; + } + } + } + + if path.is_empty() { + return Err(anyhow!("Cannot find a place to store config file")); + } + + info!(config_file=%path, "Config file will be stored here"); + + let config = Config { + path, + data: Mutex::new(config_value), + }; + config.save()?; + Ok(config) + } + + pub fn save(&self) -> Result<()> { + let data = self.data.lock().unwrap(); + let yaml = serde_yaml::to_string(&*data)?; + fs::write(&self.path, yaml)?; + Ok(()) + } + + pub fn set_value(&self, path: &[&str], value: Value) -> Result<()> { + let mut data = self.data.lock().unwrap(); + Self::set_value_internal(&mut data, path, value.clone())?; + drop(data); + self.save()?; + Ok(()) + } + + fn set_value_internal(data: &mut Value, path: &[&str], value: Value) -> Result<()> { + if path.is_empty() { + *data = value; + return Ok(()); + } + if let Value::Mapping(map) = data { + let key = path[0].to_lowercase(); + let key_value = Value::String(key.clone()); + if path.len() == 1 { + map.insert(key_value, value); + } else { + let entry = map + .entry(key_value) + .or_insert(Value::Mapping(Mapping::new())); + Self::set_value_internal(entry, &path[1..], value)?; + } + Ok(()) + } else { + Err(anyhow!("Current node is not a map")) + } + } + + pub fn get_value(&self, path: &[&str]) -> Result { + let data = self.data.lock().unwrap(); + Self::get_value_internal(&data, path) + } + + fn get_value_internal(data: &Value, path: &[&str]) -> Result { + let mut current = data; + for (i, key) in path.iter().enumerate() { + if let Value::Mapping(map) = current { + let key = key.to_lowercase(); + if let Some(next) = map.get(&Value::String(key)) { + current = next; + } else { + return Err(anyhow!("Path {} does not exist", path[..=i].join("."))); + } + } else { + return Err(anyhow!("Path {} is not a Config", path[..i].join("."))); + } + } + Ok(current.clone()) + } + + fn get_home_yml_path() -> String { + home_dir() + .map(|p| p.join(".pmomusic.yml")) + .unwrap_or_else(|| PathBuf::from(".")) + .to_string_lossy() + .to_string() + } + + fn apply_env_overrides(config: &mut Value) { + for (key, value) in env::vars() { + if key.starts_with(ENV_PREFIX) { + let key_path = key + .trim_start_matches(ENV_PREFIX) + .split("__") + .collect::>(); + let yaml_value = Self::convert_env_value(&value); + let _ = Self::set_value_internal(config, &key_path, yaml_value); + } + } + } + + fn convert_env_value(value: &str) -> Value { + if let Ok(parsed) = serde_yaml::from_str::(value) { + return parsed; + } + Value::String(value.to_string()) + } + + fn lower_keys_value(value: Value) -> Value { + match value { + Value::Mapping(map) => { + let mut new_map = Mapping::new(); + for (k, v) in map { + if let Value::String(s) = k { + let new_key = Value::String(s.to_lowercase()); + let new_val = Self::lower_keys_value(v); + new_map.insert(new_key, new_val); + } else { + new_map.insert(k, Self::lower_keys_value(v)); + } + } + Value::Mapping(new_map) + } + Value::Sequence(seq) => { + Value::Sequence(seq.into_iter().map(Self::lower_keys_value).collect()) + } + _ => value, + } + } + + fn is_writable(path: &str) -> bool { + let path = Path::new(path); + if let Some(parent) = path.parent() { + fs::metadata(parent) + .map(|m| !m.permissions().readonly()) + .unwrap_or(false) + } else { + false + } + } + + pub fn get_base_url(&self) -> String { + match self.get_value(&["host", "base_url"]) { + Ok(Value::String(s)) if !s.is_empty() => s, + Ok(_) => { + tracing::warn!("Base URL is not a string or empty, using default localhost"); + guess_local_ip() + } + Err(err) => { + tracing::warn!("Failed to get base URL: {}, using default localhost", err); + guess_local_ip() + } + } + } + + pub fn get_http_port(&self) -> u16 { + match self.get_value(&["host", "http_port"]) { + Ok(Value::Number(n)) if n.is_i64() => n.as_i64().unwrap() as u16, + Ok(Value::String(s)) => match s.parse::() { + Ok(port) => port, + Err(_) => { + tracing::warn!("Invalid HTTP port '{}', using default 8080", s); + 8080 + } + }, + Ok(_) => { + tracing::warn!("HTTP port not a number or string, using default 8080"); + 8080 + } + Err(err) => { + tracing::warn!("Failed to get HTTP port: {}, using default 8080", err); + 8080 + } + } + } + + pub fn get_device_udn(&self, devtype: &str, name: &str) -> Result { + let path = &["devices", devtype, name, "udn"]; + match self.get_value(path) { + Ok(Value::String(udn)) => Ok(udn), + _ => { + let new_udn = Uuid::new_v4().to_string(); + self.set_value(path, Value::String(new_udn.clone()))?; + Ok(new_udn) + } + } + } + + pub fn get_cover_cache_dir(&self) -> Result { + match self.get_value(&["host", "cover_cache", "directory"])? { + Value::String(s) => Ok(s), + _ => Ok("./.pmomusic_covers".to_string()), + } + } + + pub fn get_cover_cache_size(&self) -> Result { + match self.get_value(&["host", "cover_cache", "size"])? { + Value::Number(n) if n.is_i64() => Ok(n.as_i64().unwrap() as usize), + Value::Number(n) if n.is_u64() => Ok(n.as_u64().unwrap() as usize), + _ => Ok(2000), + } + } +} + +/// Retourne l'instance globale +pub fn get_config() -> Arc { + CONFIG.clone() +} diff --git a/pmoconfig/src/pmomusic.yaml b/pmoconfig/src/pmomusic.yaml new file mode 100644 index 00000000..c2acfad7 --- /dev/null +++ b/pmoconfig/src/pmomusic.yaml @@ -0,0 +1,11 @@ +host: + http_port: "8080" + cover_cache: + directory: "./.pmomusic_covers" + size: 2000 +devices: + mediarenderer: + mpd_renderer: + mediaserver: + qobuz: + udn: "uuid:28963b75-4c5f-4da7-b10e-ffafd" \ No newline at end of file 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 new file mode 100644 index 00000000..ab8bd4b2 --- /dev/null +++ b/pmoupnp/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "pmoupnp" +version = "0.1.0" +edition = "2024" + +[dependencies] +pmoconfig = { path = "../pmoconfig" } +pmodidl = { path = "../pmodidl"} + +url = "2.5.7" +uuid = "1.18.1" +hex = "0.4.3" +base64 = "0.22.1" +thiserror = "2.0.16" +xmltree = "0.11.0" +get_if_addrs = "0.5.3" +axum = "0.8.4" +tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time", "signal"] } +tokio-stream = "0.1" +futures-util = "0.3" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +chrono = { version = "0.4.42", features = ["serde"] } +log = "0.4.28" +once_cell = "1.20" +parking_lot = "0.12" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] } +futures = "0.3" +async-stream = "0.3.6" +axum-server = "0.7.2" +axum-embed = "0.1.0" +rust-embed = "8.7.2" +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" +reqwest = "0.12.23" diff --git a/pmoupnp/errors.rs b/pmoupnp/errors.rs new file mode 100644 index 00000000..f34f65a0 --- /dev/null +++ b/pmoupnp/errors.rs @@ -0,0 +1,72 @@ +use thiserror::Error; + + + +#[derive(Error, Debug)] +pub enum StateVariableError { + #[error("Conversion error: {0}")] + ConversionError(String), + + #[error("Validation error: {0}")] + ValidationError(String), + + #[error("Range error: {0}")] + RangeError(String), + + #[error("Type error: {0}")] + TypeError(String), + + #[error("Parse error: {0}")] + ParseError(String), + + #[error("Event condition error: {0}")] + EventConditionError(String), + + #[error("Arithmetic error: {0}")] + ArithmeticError(String), + + #[error("Unknown error: {0}")] + Unknown(String), +} + +impl From for StateVariableError { + fn from(err: std::num::TryFromIntError) -> Self { + StateVariableError::ConversionError(format!("Integer conversion error: {}", err)) + } +} + +impl From for StateVariableError { + fn from(err: std::str::ParseBoolError) -> Self { + StateVariableError::ConversionError(format!("Boolean conversion error: {}", err)) + } +} + +impl From for StateVariableError { + fn from(err: uuid::Error) -> Self { + StateVariableError::ConversionError(format!("UUID conversion error: {}", err)) + } +} + +impl From for StateVariableError { + fn from(err: chrono::ParseError) -> Self { + StateVariableError::ConversionError(format!("Time conversion error: {}", err)) + } +} + +impl From for StateVariableError { + fn from(err: url::ParseError) -> Self { + StateVariableError::ConversionError(format!("URI conversion error: {}", err)) + } +} + +impl From for StateVariableError { + fn from(err: base64::DecodeError) -> Self { + StateVariableError::ConversionError(format!("Base64 conversion error: {}", err)) + } +} + +impl From for StateVariableError { + fn from(err: hex::FromHexError) -> Self { + StateVariableError::ConversionError(format!("Hex conversion error: {}", err)) + } +} diff --git a/pmoupnp/src/actions/action_instance.rs b/pmoupnp/src/actions/action_instance.rs new file mode 100644 index 00000000..49ca2938 --- /dev/null +++ b/pmoupnp/src/actions/action_instance.rs @@ -0,0 +1,136 @@ +use std::sync::Arc; + +use xmltree::{Element, XMLNode}; + +use crate::actions::Action; +use crate::actions::Argument; +use crate::actions::ArgumentSet; +use crate::actions::ArgInstanceSet; +use crate::actions::ActionInstance; +use crate::UpnpInstance; +use crate::UpnpObject; +use crate::UpnpTyped; +use crate::UpnpTypedInstance; +use crate::UpnpObjectType; + +impl UpnpObject for ActionInstance { + fn to_xml_element(&self) -> Element { + let mut elem = Element::new("action"); + + // + let mut name_elem = Element::new("name"); + name_elem.children.push(XMLNode::Text(self.get_name().clone())); + elem.children.push(XMLNode::Element(name_elem)); + + // Utiliser le set d'instances d'arguments + let args_container = self.arguments.to_xml_element(); + elem.children.push(XMLNode::Element(args_container)); + + elem + } +} + +impl UpnpTyped for ActionInstance { + fn as_upnp_object_type(&self) -> &UpnpObjectType { + return &self.object; + } +} + +impl UpnpInstance for ActionInstance { + + type Model = Action; + + fn new(action: &Action) -> Self { + // Créer les instances d'arguments + let mut arguments = ArgInstanceSet::new(); + + for arg_model in action.arguments().all() { + let arg_instance = Arc::new(crate::actions::ArgumentInstance::new(&*arg_model)); + if let Err(e) = arguments.insert(arg_instance) { + tracing::error!("Failed to insert argument instance: {:?}", e); + } + } + + Self { + object: UpnpObjectType { + name: action.get_name().clone(), + object_type: "ActionInstance".to_string(), + }, + model: action.clone(), + arguments, // ⬅️ Set d'instances, pas le modèle ! + } + } + +} + + +impl UpnpTypedInstance for ActionInstance { + + fn get_model(&self) -> &Self::Model { + &self.model + } +} + +impl ActionInstance { + /// Retourne une instance d'argument par son nom. + /// + /// # Arguments + /// + /// * `name` - Nom de l'argument à rechercher + /// + /// # Returns + /// + /// `Some(Arc)` si trouvé, `None` sinon. + pub fn argument(&self, name: &str) -> Option> { + self.arguments.get_by_name(name) + } + + /// Retourne le set d'instances d'arguments. + /// + /// # Returns + /// + /// Référence vers le `ArgInstanceSet` contenant toutes les instances. + /// + /// # Examples + /// + /// ```ignore + /// for arg_instance in action_instance.arguments_set().all() { + /// println!("Argument: {}", arg_instance.get_name()); + /// if let Some(var) = arg_instance.get_variable_instance() { + /// println!(" Variable: {} = {}", var.get_name(), var.value()); + /// } + /// } + /// ``` + pub fn arguments_set(&self) -> &ArgInstanceSet { + &self.arguments // ⬅️ Retourne les INSTANCES, pas les modèles ! + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::actions::Action; + use crate::UpnpInstance; + + #[test] + fn test_action_instance_creation() { + let action = Action::new("Play".to_string()); + let instance = ActionInstance::new(&action); + + assert_eq!(instance.get_name(), "Play"); + } + + #[test] + fn test_action_instance_has_argument_instances() { + let action = Action::new("Play".to_string()); + let instance = ActionInstance::new(&action); + + // Vérifier que arguments_set() retourne bien des instances + assert!(instance.arguments_set().all().iter().all(|arg| { + // Chaque argument doit être une ArgumentInstance + arg.get_model(); // Cette méthode existe seulement sur les instances + true + })); + } +} + diff --git a/pmoupnp/src/actions/action_instance_set.rs b/pmoupnp/src/actions/action_instance_set.rs new file mode 100644 index 00000000..ec2725ca --- /dev/null +++ b/pmoupnp/src/actions/action_instance_set.rs @@ -0,0 +1,21 @@ +use crate::{ + UpnpObject, + actions::{ActionInstanceSet}, +}; + +use xmltree::{Element,XMLNode}; + +impl UpnpObject for ActionInstanceSet { + // Méthode pour convertir en XML (à implémenter avec une librairie XML) + fn to_xml_element(&self) -> Element { + let mut elem = Element::new("actionList"); + + for action in self.all() { + let action_elem = action.to_xml_element(); // retourne un complet + elem.children.push(XMLNode::Element(action_elem)); + } + + elem + } +} + diff --git a/pmoupnp/src/actions/action_methods.rs b/pmoupnp/src/actions/action_methods.rs new file mode 100644 index 00000000..2ac95758 --- /dev/null +++ b/pmoupnp/src/actions/action_methods.rs @@ -0,0 +1,62 @@ +use std::sync::Arc; + +use xmltree::{Element, XMLNode}; + +use crate::UpnpModel; +use crate::UpnpObject; +use crate::UpnpObjectSetError; +use crate::UpnpObjectType; +use crate::UpnpTyped; +use crate::actions::Action; +use crate::actions::ActionInstance; +use crate::actions::Argument; +use crate::actions::ArgumentSet; + +impl UpnpObject for Action { + fn to_xml_element(&self) -> Element { + let mut action_elem = Element::new("action"); + + // + let mut name_elem = Element::new("name"); + name_elem + .children + .push(XMLNode::Text(self.get_name().clone())); + action_elem.children.push(XMLNode::Element(name_elem)); + + // + let args_elem = self.arguments.to_xml_element(); + action_elem.children.push(XMLNode::Element(args_elem)); + + action_elem + } +} + +impl UpnpModel for Action { + type Instance = ActionInstance; +} + +impl UpnpTyped for Action { + fn as_upnp_object_type(&self) -> &UpnpObjectType { + return &self.object; + } +} + +impl Action { + pub fn new(name: String) -> Action { + Self { + object: UpnpObjectType { + name, + object_type: "Action".to_string(), + }, + arguments: ArgumentSet::new(), + } + } + + pub fn add_argument(&mut self, arg: Arc) -> Result<(), UpnpObjectSetError> { + self.arguments.insert(arg) + } + + pub fn arguments(&self) -> &ArgumentSet { + &self.arguments + } +} diff --git a/pmoupnp/src/actions/action_set_methods.rs b/pmoupnp/src/actions/action_set_methods.rs new file mode 100644 index 00000000..4574a088 --- /dev/null +++ b/pmoupnp/src/actions/action_set_methods.rs @@ -0,0 +1,23 @@ +use xmltree::{Element, XMLNode}; + +use crate::actions::{ActionInstanceSet, ActionSet}; +use crate::{UpnpModel, UpnpObject}; + +impl UpnpObject for ActionSet { + fn to_xml_element(&self) -> Element { + let mut elem = Element::new("actionList"); + + for action in self.all() { + let action_elem = action.to_xml_element(); // retourne un complet + elem.children.push(XMLNode::Element(action_elem)); + } + + elem + } + +} + + +impl UpnpModel for ActionSet { + type Instance = ActionInstanceSet; +} \ No newline at end of file diff --git a/pmoupnp/src/actions/arg_inst_set_methods.rs b/pmoupnp/src/actions/arg_inst_set_methods.rs new file mode 100644 index 00000000..92bc0f84 --- /dev/null +++ b/pmoupnp/src/actions/arg_inst_set_methods.rs @@ -0,0 +1,31 @@ +use std::collections::HashMap; + +use std::sync::RwLock; +use xmltree::{Element, XMLNode}; + +use crate::{actions::{ArgInstanceSet, ArgumentSet}, UpnpObject}; + +use crate::UpnpInstance; + +impl UpnpObject for ArgInstanceSet { + fn to_xml_element(&self) -> Element { + let mut elem = Element::new("serviceStateTable"); + + for state_var in self.all() { + let state_var_elem = state_var.to_xml_element(); // retourne un complet + elem.children.push(XMLNode::Element(state_var_elem)); + } + + elem + } +} + +impl UpnpInstance for ArgInstanceSet { + type Model = ArgumentSet; + + fn new(_: &ArgumentSet) -> Self { + Self { objects: RwLock::new(HashMap::new()) } + } +} + + diff --git a/pmoupnp/src/actions/arg_instance_methods.rs b/pmoupnp/src/actions/arg_instance_methods.rs new file mode 100644 index 00000000..7ad75e16 --- /dev/null +++ b/pmoupnp/src/actions/arg_instance_methods.rs @@ -0,0 +1,256 @@ +use std::{collections::HashMap, sync::{Arc, RwLock}}; + +use xmltree::Element; + +use crate::{actions::{ActionInstanceSet, ActionSet, Argument, ArgumentInstance}, state_variables::StateVarInstance, UpnpInstance, UpnpObject, UpnpObjectType, UpnpTyped, UpnpTypedInstance}; + + +impl UpnpObject for ArgumentInstance { + fn to_xml_element(&self) -> Element { + self.get_model().to_xml_element() + } +} + +impl UpnpTyped for ArgumentInstance { + fn as_upnp_object_type(&self) -> &UpnpObjectType { + return &self.object; + } +} + +/// Implémentation de [`UpnpTypedInstance`] pour [`ArgumentInstance`]. +/// +/// Cette implémentation permet d'accéder au modèle [`Argument`] depuis l'instance +/// via la méthode [`get_model()`](UpnpTypedInstance::get_model). +/// +/// # Examples +/// +/// ```ignore +/// use pmoupnp::UpnpTypedInstance; +/// +/// let arg_instance = ArgumentInstance::new(&arg_model); +/// +/// // Accéder au modèle +/// let model = arg_instance.get_model(); +/// println!("Direction: in={}, out={}", model.is_in(), model.is_out()); +/// println!("Related variable: {}", model.state_variable().get_name()); +/// ``` +impl UpnpTypedInstance for ArgumentInstance { + /// Retourne une référence vers le modèle [`Argument`]. + /// + /// Permet d'accéder aux métadonnées statiques définies dans le modèle : + /// - Direction de l'argument (in/out) + /// - Variable d'état associée + /// - Nom et type + fn get_model(&self) -> &Self::Model { + &self.model + } +} + + +/// Implémentation de [`UpnpInstance`] pour [`ArgumentInstance`]. +/// +/// Cette implémentation fournit le constructeur standard qui crée une instance +/// **non liée** d'un argument. La liaison à une [`StateVarInstance`] doit être +/// effectuée séparément via [`bind_variable`](ArgumentInstance::bind_variable). +/// +/// # Processus de construction en deux phases +/// +/// ```text +/// Phase 1 (new) Phase 2 (bind_variable) +/// ┌─────────────────┐ ┌──────────────────────┐ +/// │ ArgumentInstance│ │ StateVarInstance │ +/// │ │ │ │ +/// │ model: Arc<...> │────>│ Liaison établie │ +/// │ variable: None │ │ variable: Some(...) │ +/// └─────────────────┘ └──────────────────────┘ +/// ↓ ↓ +/// Création bind_variable(&var) +/// ``` +/// +/// # Pourquoi deux phases ? +/// +/// 1. **Ordre de création** : Les modèles (`Argument`) existent avant les instances +/// 2. **Validation différée** : Les dépendances sont vérifiées après instanciation +/// 3. **Découplage** : Permet de créer des arguments même si les variables n'existent pas encore +/// +/// # Examples +/// +/// ```ignore +/// use pmoupnp::actions::{Argument, ArgumentInstance}; +/// use pmoupnp::UpnpInstance; +/// +/// let arg_model = Argument::new_in("InstanceID".to_string(), instance_id_var); +/// +/// // Création de l'instance - Phase 1 +/// let arg_instance = ArgumentInstance::new(&arg_model); +/// +/// // À ce stade, l'instance existe mais n'est pas encore liée +/// assert_eq!(arg_instance.get_name(), "InstanceID"); +/// assert!(arg_instance.get_variable_instance().is_none()); +/// +/// // La liaison se fera plus tard via bind_variable() +/// ``` +impl UpnpInstance for ArgumentInstance { + type Model = Argument; + + /// Crée une nouvelle instance d'argument depuis son modèle. + /// + /// # Arguments + /// + /// * `from` - Référence vers le modèle [`Argument`] définissant cet argument + /// + /// # Returns + /// + /// Une nouvelle `ArgumentInstance` avec : + /// - Nom copié depuis le modèle + /// - Référence vers le modèle (clone) + /// - `variable_instance` initialisé à `None` (liaison non établie) + /// + /// # État initial + /// + /// L'instance créée n'est **pas encore liée** à une variable d'état. + /// Pour établir la liaison, appelez [`bind_variable`](ArgumentInstance::bind_variable). + /// + /// # Thread-safety + /// + /// L'instance retournée est thread-safe et peut être partagée via `Arc`. + /// + /// # Examples + /// + /// ```ignore + /// use pmoupnp::UpnpInstance; + /// + /// // Création depuis un modèle + /// let instance = ArgumentInstance::new(&arg_model); + /// + /// // L'instance hérite des propriétés du modèle + /// assert_eq!(instance.get_name(), arg_model.get_name()); + /// assert_eq!(instance.is_in(), arg_model.is_in()); + /// + /// // Mais n'a pas encore de valeur runtime + /// assert!(instance.get_variable_instance().is_none()); + /// ``` + fn new(from: &Argument) -> Self { + Self { + // Copie des métadonnées depuis le modèle + object: UpnpObjectType { + name: from.get_name().clone(), + object_type: "ArgumentInstance".to_string(), + }, + + // Clone du modèle pour référence future + model: from.clone(), + + // Initialisation à None - sera lié plus tard via bind_variable() + // Arc> permet la modification thread-safe post-construction + variable_instance: Arc::new(RwLock::new(None)), + } + } +} + +// ============================================================================ +// Méthodes de liaison et d'accès +// ============================================================================ + +impl ArgumentInstance { + /// Lie cet argument à une instance de variable d'état. + /// + /// Cette méthode établit la connexion entre l'argument et sa variable d'état, + /// permettant l'accès aux valeurs runtime lors de l'exécution d'actions. + /// + /// # Arguments + /// + /// * `var_instance` - Instance de la variable d'état à lier + /// + /// # Thread-safety + /// + /// Cette méthode acquiert un **write lock** sur `variable_instance` et peut + /// bloquer si d'autres threads lisent actuellement la valeur. + /// + /// # Panics + /// + /// Panique si le lock est empoisonné (poisoned), ce qui ne devrait jamais + /// arriver dans un usage normal. + /// + /// # Examples + /// + /// ```ignore + /// use std::sync::Arc; + /// + /// let arg_instance = ArgumentInstance::new(&arg_model); + /// let var_instance = Arc::new(StateVarInstance::new(&state_var)); + /// + /// // Établir la liaison + /// arg_instance.bind_variable(var_instance.clone()); + /// + /// // Vérifier que la liaison est établie + /// assert!(arg_instance.get_variable_instance().is_some()); + /// ``` + /// + /// # Note + /// + /// Cette méthode peut être appelée plusieurs fois pour changer la variable liée, + /// bien que ce ne soit généralement pas recommandé dans un usage normal. + pub fn bind_variable(&self, var_instance: Arc) { + let mut var = self.variable_instance.write().unwrap(); + *var = Some(var_instance); + } + + /// Retourne l'instance de variable d'état liée, si elle existe. + /// + /// # Returns + /// + /// - `Some(Arc)` si une variable est liée + /// - `None` si aucune liaison n'a été établie via [`bind_variable`](Self::bind_variable) + /// + /// # Thread-safety + /// + /// Cette méthode acquiert un **read lock** sur `variable_instance`. + /// Plusieurs threads peuvent lire simultanément sans blocage. + /// + /// # Panics + /// + /// Panique si le lock est empoisonné (poisoned). + /// + /// # Examples + /// + /// ```ignore + /// // Vérifier si la liaison existe + /// if let Some(var) = arg_instance.get_variable_instance() { + /// println!("Variable liée : {}", var.get_name()); + /// println!("Valeur actuelle : {}", var.value()); + /// } else { + /// println!("Aucune variable liée"); + /// } + /// ``` + /// + /// # Usage dans l'exécution d'actions + /// + /// ```ignore + /// async fn execute_action(action: &ActionInstance) -> Result<(), ActionError> { + /// for arg in action.arguments_set().all() { + /// if let Some(var) = arg.get_variable_instance() { + /// // Utiliser var.value() pour lire/écrire + /// println!("Paramètre {} = {}", arg.get_name(), var.value()); + /// } else { + /// return Err(ActionError::UnboundArgument(arg.get_name().to_string())); + /// } + /// } + /// Ok(()) + /// } + /// ``` + pub fn get_variable_instance(&self) -> Option> { + self.variable_instance.read().unwrap().clone() + } +} + + +impl UpnpInstance for ActionInstanceSet { + type Model = ActionSet; + + fn new(_: &ActionSet) -> Self { + Self { + objects: RwLock::new(HashMap::new()) + } + } +} \ No newline at end of file diff --git a/pmoupnp/src/actions/arg_set_methods.rs b/pmoupnp/src/actions/arg_set_methods.rs new file mode 100644 index 00000000..a8f7cae5 --- /dev/null +++ b/pmoupnp/src/actions/arg_set_methods.rs @@ -0,0 +1,29 @@ +use crate::actions::ArgInstanceSet; +use crate::UpnpModel; +use crate::{ + UpnpObject, + actions::{ArgumentSet}, +}; +use xmltree::Element; + +impl UpnpObject for ArgumentSet { + // Méthode pour convertir en XML (à implémenter avec une librairie XML) + fn to_xml_element(&self) -> Element { + let mut elem = Element::new("argumentList"); + + for arg in self.all() { + let arg_elem = arg.to_xml_element(); // toujours un contenant 1 ou 2 + + // Pour InOut, on ajoute tous les enfants du généré + for child in arg_elem.children { + elem.children.push(child); + } + } + + elem + } +} + +impl UpnpModel for ArgumentSet { + type Instance = ArgInstanceSet; +} diff --git a/pmoupnp/src/actions/argument_methods.rs b/pmoupnp/src/actions/argument_methods.rs new file mode 100644 index 00000000..84e6e69a --- /dev/null +++ b/pmoupnp/src/actions/argument_methods.rs @@ -0,0 +1,116 @@ +use std::sync::Arc; + +use xmltree::{Element, XMLNode}; + +use crate::{ + actions::{Argument, ArgumentInstance}, state_variables::StateVariable, UpnpModel, UpnpObject, UpnpObjectType, UpnpTyped +}; + +impl UpnpTyped for Argument { + fn as_upnp_object_type(&self) -> &UpnpObjectType { + &self.object + } +} + +impl UpnpObject for Argument { + fn to_xml_element(&self) -> Element { + let mut parent = Element::new("argumentList"); + + if self.is_in() && self.is_out() { + // InOut → deux arguments + parent.children.push(XMLNode::Element(make_argument_elem( + self.get_name(), + "in", + self.state_variable().get_name(), + ))); + parent.children.push(XMLNode::Element(make_argument_elem( + self.get_name(), + "out", + self.state_variable().get_name(), + ))); + } else { + // Cas simple + let direction = if self.is_in() { "in" } else { "out" }; + parent.children.push(XMLNode::Element(make_argument_elem( + self.get_name(), + direction, + self.state_variable().get_name(), + ))); + } + + parent + } +} + +impl UpnpModel for Argument { + type Instance = ArgumentInstance; +} + + + +impl Argument { + fn new(name: String, state_variable: Arc) -> Self { + Self { + object: UpnpObjectType { + name, + object_type: "Argument".to_string(), + }, + state_variable, + is_in: false, + is_out: false, + } + } + + pub fn new_in(name: String, state_variable: Arc) -> Self { + let mut arg = Self::new(name, state_variable); + arg.is_in = true; + arg + } + + pub fn new_out(name: String, state_variable: Arc) -> Self { + let mut arg = Self::new(name, state_variable); + arg.is_out = true; + arg + } + + pub fn new_in_out(name: String, state_variable: Arc) -> Self { + let mut arg = Self::new(name, state_variable); + arg.is_in = true; + arg.is_out = true; + arg + } + + pub fn state_variable(&self) -> &StateVariable { + &self.state_variable + } + + pub fn is_in(&self) -> bool { + self.is_in + } + + pub fn is_out(&self) -> bool { + self.is_out + } +} + +/// Fabrique un complet avec ses sous-éléments +fn make_argument_elem(name: &str, direction: &str, state_var_name: &str) -> Element { + let mut arg = Element::new("argument"); + + let mut name_elem = Element::new("name"); + name_elem.children.push(XMLNode::Text(name.to_string())); + + let mut dir_elem = Element::new("direction"); + dir_elem.children.push(XMLNode::Text(direction.to_string())); + + let mut rel_elem = Element::new("relatedStateVariable"); + rel_elem + .children + .push(XMLNode::Text(state_var_name.to_string())); + + arg.children.push(XMLNode::Element(name_elem)); + arg.children.push(XMLNode::Element(dir_elem)); + arg.children.push(XMLNode::Element(rel_elem)); + + arg +} diff --git a/pmoupnp/src/actions/errors.rs b/pmoupnp/src/actions/errors.rs new file mode 100644 index 00000000..409d5045 --- /dev/null +++ b/pmoupnp/src/actions/errors.rs @@ -0,0 +1,37 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ActionError { + #[error("Action error: {0}")] + GeneralError(String), + + #[error("Argument error: {0}")] + ArgumentError(String), + + #[error("Set operation error: {0}")] + SetError(String), +} + +impl From for ActionError { + fn from(err: std::io::Error) -> Self { + ActionError::GeneralError(format!("IO error: {}", err)) + } +} + +#[derive(Error, Debug)] +pub enum ArgumentError { + #[error("Argument error: {0}")] + GeneralError(String), + + #[error("Argument error: {0}")] + ArgumentError(String), + + #[error("Set operation error: {0}")] + SetError(String), +} + +impl From for ArgumentError { + fn from(err: std::io::Error) -> Self { + ArgumentError::GeneralError(format!("IO error: {}", err)) + } +} \ No newline at end of file diff --git a/pmoupnp/src/actions/macros.rs b/pmoupnp/src/actions/macros.rs new file mode 100644 index 00000000..987e1885 --- /dev/null +++ b/pmoupnp/src/actions/macros.rs @@ -0,0 +1,280 @@ +/// Macro pour définir facilement une action UPnP. +/// +/// Cette macro simplifie la création d'actions UPnP statiques en générant +/// automatiquement le code nécessaire pour initialiser une action avec ses arguments. +/// +/// # Syntaxe +/// +/// ## Action avec arguments +/// +/// ```ignore +/// define_action! { +/// pub static ACTION_NAME = "ActionName" { +/// in "ParamName" => VARIABLE_REF, +/// out "ResultParam" => RESULT_VAR, +/// } +/// } +/// ``` +/// +/// ## Action sans arguments +/// +/// ```ignore +/// define_action! { +/// pub static ACTION_NAME = "ActionName" +/// } +/// ``` +/// +/// # Arguments +/// +/// - `ACTION_NAME` : Nom de la constante statique Rust +/// - `"ActionName"` : Nom de l'action UPnP (chaîne littérale) +/// - `in` ou `out` : Direction de l'argument (entrée ou sortie) +/// - `"ParamName"` : Nom du paramètre UPnP (chaîne littérale) +/// - `VARIABLE_REF` : Référence vers une `Lazy>` +/// +/// # Type de retour +/// +/// La macro génère une `Lazy>` qui sera initialisée lors du premier accès. +/// +/// # Prérequis +/// +/// Les variables d'état référencées doivent être définies comme : +/// +/// ```ignore +/// pub static MY_VAR: Lazy> = Lazy::new(|| { +/// Arc::new(StateVariable::new(StateVarType::UI4, "MyVar".to_string())) +/// }); +/// ``` +/// +/// # Examples +/// +/// ```ignore +/// use once_cell::sync::Lazy; +/// use std::sync::Arc; +/// +/// // Définir les variables d'état +/// pub static INSTANCE_ID: Lazy> = Lazy::new(|| { +/// Arc::new(StateVariable::new(StateVarType::UI4, "InstanceID".to_string())) +/// }); +/// +/// pub static TRANSPORT_URI: Lazy> = Lazy::new(|| { +/// Arc::new(StateVariable::new(StateVarType::String, "TransportURI".to_string())) +/// }); +/// +/// // Définir une action avec arguments +/// define_action! { +/// pub static PLAY = "Play" { +/// in "InstanceID" => INSTANCE_ID, +/// in "Speed" => TRANSPORT_SPEED, +/// } +/// } +/// +/// // Action sans arguments +/// define_action! { +/// pub static PAUSE = "Pause" +/// } +/// +/// // Utilisation +/// fn main() { +/// let play_action = &*PLAY; // Déréférence la Lazy> +/// println!("Action: {}", play_action.get_name()); +/// } +/// ``` +/// +/// # Notes d'implémentation +/// +/// - Les `Arc` sont clonés (shallow copy du pointeur) +/// - Chaque `Argument` est wrappé dans un `Arc` +/// - L'`Action` finale est wrappée dans un `Arc` +/// - Initialisation paresseuse via `Lazy` (thread-safe) +#[macro_export] +macro_rules! define_action { + // Variante sans arguments + (pub static $name:ident = $action_name:literal) => { + pub static $name: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| { + std::sync::Arc::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> = + 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) + ); + )* + + std::sync::Arc::new(ac) + }); + }; + + // Helper interne pour créer un argument d'entrée + (@arg in $name:literal, $var:expr) => { + std::sync::Arc::new( + $crate::actions::Argument::new_in( + $name.to_string(), + std::sync::Arc::clone(&$var) + ) + ) + }; + + // Helper interne pour créer un argument de sortie + (@arg out $name:literal, $var:expr) => { + std::sync::Arc::new( + $crate::actions::Argument::new_out( + $name.to_string(), + std::sync::Arc::clone(&$var) + ) + ) + }; +} + +/// Macro pour définir plusieurs actions UPnP en une seule déclaration. +/// +/// Cette macro permet de regrouper la définition de plusieurs actions pour +/// améliorer la lisibilité et réduire la répétition de code. +/// +/// # Syntaxe +/// +/// ```ignore +/// define_actions! { +/// ACTION1 = "Action1" { +/// in "Param1" => VAR1, +/// out "Result1" => VAR2, +/// } +/// +/// ACTION2 = "Action2" { +/// in "Param1" => VAR1, +/// } +/// +/// ACTION3 = "Action3" +/// } +/// ``` +/// +/// # Arguments +/// +/// Chaque action suit la même syntaxe que [`define_action!`], mais sans +/// le mot-clé `pub static`. +/// +/// # Type de retour +/// +/// Génère une `Lazy>` pour chaque action définie. +/// +/// # Examples +/// +/// ```ignore +/// use once_cell::sync::Lazy; +/// use std::sync::Arc; +/// +/// // Variables d'état +/// pub static INSTANCE_ID: Lazy> = Lazy::new(|| { +/// Arc::new(StateVariable::new(StateVarType::UI4, "InstanceID".to_string())) +/// }); +/// +/// pub static TRANSPORT_URI: Lazy> = Lazy::new(|| { +/// Arc::new(StateVariable::new(StateVarType::String, "TransportURI".to_string())) +/// }); +/// +/// pub static URI_METADATA: Lazy> = Lazy::new(|| { +/// Arc::new(StateVariable::new(StateVarType::String, "URIMetaData".to_string())) +/// }); +/// +/// // Définir plusieurs actions ensemble +/// define_actions! { +/// PLAY = "Play" { +/// in "InstanceID" => INSTANCE_ID, +/// } +/// +/// STOP = "Stop" { +/// in "InstanceID" => INSTANCE_ID, +/// } +/// +/// PAUSE = "Pause" { +/// in "InstanceID" => INSTANCE_ID, +/// } +/// +/// SET_AV_TRANSPORT_URI = "SetAVTransportURI" { +/// in "InstanceID" => INSTANCE_ID, +/// in "CurrentURI" => TRANSPORT_URI, +/// in "CurrentURIMetaData" => URI_METADATA, +/// } +/// } +/// +/// // Utilisation +/// fn setup_transport_service() { +/// let actions = vec![&*PLAY, &*STOP, &*PAUSE, &*SET_AV_TRANSPORT_URI]; +/// for action in actions { +/// println!("Action: {}", action.get_name()); +/// } +/// } +/// ``` +/// +/// # Avantages +/// +/// - Regroupement logique des actions d'un service +/// - Réduction de la répétition de `pub static` et `define_action!` +/// - Meilleure lisibilité pour les services avec nombreuses actions +/// +/// # Notes +/// +/// - Toutes les actions définies sont publiques (`pub`) +/// - Chaque action est indépendante et peut être utilisée séparément +/// - La macro se développe en plusieurs appels à [`define_action!`] +#[macro_export] +macro_rules! define_actions { + // Variante avec arguments pour chaque action + ( + $( + $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),* + } + } + )* + }; + + // Variante mixte : actions avec et sans arguments + ( + $( + $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),* + } + } + )? + $( + // Cas sans accolades (action sans arguments) + #[allow(unused)] + define_action! { + pub static $name = $action_name + } + )? + )* + }; +} \ No newline at end of file diff --git a/pmoupnp/src/actions/mod.rs b/pmoupnp/src/actions/mod.rs new file mode 100644 index 00000000..c58fa696 --- /dev/null +++ b/pmoupnp/src/actions/mod.rs @@ -0,0 +1,116 @@ +mod errors; + +mod action_instance; +mod action_instance_set; +mod action_methods; +mod action_set_methods; +mod arg_inst_set_methods; +mod arg_instance_methods; +mod arg_set_methods; +mod argument_methods; + +mod macros; + +use crate::{ + UpnpObjectSet, UpnpObjectType, + state_variables::{StateVarInstance, StateVariable}, +}; +use std::sync::{Arc, RwLock}; + +pub use errors::ActionError; + +#[derive(Debug, Clone)] +pub struct Action { + object: UpnpObjectType, + arguments: ArgumentSet, +} + +pub type ActionSet = UpnpObjectSet; + +#[derive(Debug, Clone)] +pub struct ActionInstance { + object: UpnpObjectType, + model: Action, + arguments: ArgInstanceSet, +} + +pub type ActionInstanceSet = UpnpObjectSet; + +#[derive(Debug, Clone)] +pub struct Argument { + object: UpnpObjectType, + state_variable: Arc, + is_in: bool, + is_out: bool, +} + +pub type ArgumentSet = UpnpObjectSet; + +/// Instance d'un argument d'action UPnP. +/// +/// Un `ArgumentInstance` représente un argument concret utilisé lors de l'exécution +/// d'une action. Contrairement au modèle [`Argument`] qui définit la structure, +/// l'instance maintient une liaison dynamique vers une [`StateVarInstance`] qui +/// contient la valeur runtime. +/// +/// # Cycle de vie +/// +/// 1. **Création** : Instanciation via [`UpnpInstance::new`] avec `variable_instance = None` +/// 2. **Liaison** : Association à une [`StateVarInstance`] via [`bind_variable`](Self::bind_variable) +/// 3. **Utilisation** : Accès à la valeur runtime via [`get_variable_instance`](Self::get_variable_instance) +/// +/// # Pourquoi `variable_instance` est optionnel ? +/// +/// La liaison ne peut pas être faite dans le constructeur car : +/// - Les `StateVarInstance` sont créées **après** les modèles +/// - Les `ActionInstance` sont créées **avant** que toutes les variables soient disponibles +/// - La validation des dépendances se fait en deux phases +/// +/// # Thread-safety +/// +/// Le champ `variable_instance` est protégé par un `RwLock` pour permettre : +/// - La liaison après création (write lock) +/// - L'accès concurrent en lecture (read lock) +/// - L'utilisation dans un contexte multi-thread +/// +/// # Examples +/// +/// ```ignore +/// use pmoupnp::actions::{Argument, ArgumentInstance}; +/// use pmoupnp::state_variables::StateVarInstance; +/// use std::sync::Arc; +/// +/// // Phase 1 : Créer l'instance (sans liaison) +/// let arg_model = Argument::new_in("Volume".to_string(), volume_var); +/// let arg_instance = ArgumentInstance::new(&arg_model); +/// assert!(arg_instance.get_variable_instance().is_none()); +/// +/// // Phase 2 : Lier à une variable d'état +/// let var_instance = Arc::new(StateVarInstance::new(&volume_var)); +/// arg_instance.bind_variable(var_instance.clone()); +/// assert!(arg_instance.get_variable_instance().is_some()); +/// +/// // Phase 3 : Utiliser la valeur runtime +/// if let Some(var) = arg_instance.get_variable_instance() { +/// println!("Current value: {}", var.value()); +/// } +/// ``` +#[derive(Debug, Clone)] +pub struct ArgumentInstance { + /// Métadonnées de l'objet UPnP + object: UpnpObjectType, + + /// Référence vers le modèle définissant la structure + model: Argument, + + /// Liaison optionnelle vers l'instance de variable d'état. + /// + /// - `None` : Pas encore liée (état initial après construction) + /// - `Some(Arc)` : Liée et prête à l'emploi + /// + /// Protégée par `RwLock` pour permettre la liaison post-construction + /// et l'accès concurrent en lecture. + variable_instance: Arc>>>, +} + +pub type ArgInstanceSet = UpnpObjectSet; diff --git a/pmoupnp/src/devices/mod.rs b/pmoupnp/src/devices/mod.rs new file mode 100644 index 00000000..e69de29b diff --git a/pmoupnp/src/lib.rs b/pmoupnp/src/lib.rs new file mode 100644 index 00000000..c3b74694 --- /dev/null +++ b/pmoupnp/src/lib.rs @@ -0,0 +1,34 @@ +mod object_trait; +mod object_set; + +pub mod actions; +pub mod mediarenderer; +pub mod server; +pub mod services; +pub mod state_variables; +pub mod value_ranges; +pub mod variable_types; + + +use std::{collections::HashMap, sync::Arc}; + +use std::sync::RwLock; + +pub use crate::object_trait::*; + +#[derive(Debug, Clone)] +pub struct UpnpObjectType { + name: String, + object_type: String, +} + +#[derive(Debug)] +pub struct UpnpObjectSet { + objects: RwLock>>, +} + +#[derive(Debug)] +pub enum UpnpObjectSetError { + AlreadyExists(String), +} + diff --git a/pmoupnp/src/mediarenderer/avtransport/actions/getcurrenttransportactions.rs b/pmoupnp/src/mediarenderer/avtransport/actions/getcurrenttransportactions.rs new file mode 100644 index 00000000..e59a1b8d --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/actions/getcurrenttransportactions.rs @@ -0,0 +1,8 @@ +use crate::mediarenderer::avtransport::variables::A_ARG_TYPE_INSTANCE_ID; +use crate::define_action; + +define_action! { + pub static GETCURRENTTRANSPORTACTIONS = "GetCurrentTransportActions" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + } +} diff --git a/pmoupnp/src/mediarenderer/avtransport/actions/getdevicecapabilities.rs b/pmoupnp/src/mediarenderer/avtransport/actions/getdevicecapabilities.rs new file mode 100644 index 00000000..2b39aed5 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/actions/getdevicecapabilities.rs @@ -0,0 +1,8 @@ +use crate::mediarenderer::avtransport::variables::A_ARG_TYPE_INSTANCE_ID; +use crate::define_action; + +define_action! { + pub static GETDEVICECAPABILITIES = "GetDeviceCapabilities" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + } +} diff --git a/pmoupnp/src/mediarenderer/avtransport/actions/getmediainfo.rs b/pmoupnp/src/mediarenderer/avtransport/actions/getmediainfo.rs new file mode 100644 index 00000000..59000649 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/actions/getmediainfo.rs @@ -0,0 +1,14 @@ +use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, NUMBEROFTRACKS, CURRENTTRACK, AVTRANSPORTURI, AVTRANSPORTURIMETADATA, AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA}; +use crate::define_action; + +define_action! { + pub static GETMEDIAINFO = "GetMediaInfo" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + out "NrTracks" => NUMBEROFTRACKS, + out "CurrentTrack" => CURRENTTRACK, + out "CurrentURI" => AVTRANSPORTURI, + out "CurrentURIMetaData" => AVTRANSPORTURIMETADATA, + out "NextURI" => AVTRANSPORTNEXTURI, + out "NextURIMetaData" => AVTRANSPORTNEXTURIMETADATA, + } +} diff --git a/pmoupnp/src/mediarenderer/avtransport/actions/getpositioninfo.rs b/pmoupnp/src/mediarenderer/avtransport/actions/getpositioninfo.rs new file mode 100644 index 00000000..6e021b72 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/actions/getpositioninfo.rs @@ -0,0 +1,14 @@ +use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, CURRENTTRACK, CURRENTTRACKDURATION, AVTRANSPORTURI, AVTRANSPORTURIMETADATA, RELATIVETIMEPOSITION, ABSOLUTETIMEPOSITION}; +use crate::define_action; + +define_action! { + pub static GETPOSITIONINFO = "GetPositionInfo" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + out "Track" => CURRENTTRACK, + out "TrackDuration" => CURRENTTRACKDURATION, + out "TrackURI" => AVTRANSPORTURI, + out "TrackMetaData" => AVTRANSPORTURIMETADATA, + out "RelTime" => RELATIVETIMEPOSITION, + out "AbsTime" => ABSOLUTETIMEPOSITION, + } +} diff --git a/pmoupnp/src/mediarenderer/avtransport/actions/gettransportinfo.rs b/pmoupnp/src/mediarenderer/avtransport/actions/gettransportinfo.rs new file mode 100644 index 00000000..45426d92 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/actions/gettransportinfo.rs @@ -0,0 +1,10 @@ +use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, TRANSPORTSTATE, TRANSPORTSTATUS}; +use crate::define_action; + +define_action! { + pub static GETTRANSPORTINFO = "GetTransportInfo" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + out "CurrentTransportState" => TRANSPORTSTATE, + out "CurrentTransportStatus" => TRANSPORTSTATUS, + } +} diff --git a/pmoupnp/src/mediarenderer/avtransport/actions/gettransportsettings.rs b/pmoupnp/src/mediarenderer/avtransport/actions/gettransportsettings.rs new file mode 100644 index 00000000..281a1828 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/actions/gettransportsettings.rs @@ -0,0 +1,8 @@ +use crate::mediarenderer::avtransport::variables::A_ARG_TYPE_INSTANCE_ID; +use crate::define_action; + +define_action! { + pub static GETTRANSPORTSETTINGS = "GetTransportSettings" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + } +} diff --git a/pmoupnp/src/mediarenderer/avtransport/actions/mod.rs b/pmoupnp/src/mediarenderer/avtransport/actions/mod.rs new file mode 100644 index 00000000..9b0ad316 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/actions/mod.rs @@ -0,0 +1,30 @@ +mod getcurrenttransportactions; +mod getdevicecapabilities; +mod getmediainfo; +mod getpositioninfo; +mod gettransportinfo; +mod gettransportsettings; +mod next; +mod pause; +mod play; +mod previous; +mod seek; +mod setavtransportnexturi; +mod setavtransporturi; +mod stop; + +pub use getcurrenttransportactions::GETCURRENTTRANSPORTACTIONS; +pub use getdevicecapabilities::GETDEVICECAPABILITIES; +pub use getmediainfo::GETMEDIAINFO; +pub use getpositioninfo::GETPOSITIONINFO; +pub use gettransportinfo::GETTRANSPORTINFO; +pub use gettransportsettings::GETTRANSPORTSETTINGS; +pub use next::NEXT; +pub use pause::PAUSE; +pub use play::PLAY; +pub use previous::PREVIOUS; +pub use seek::SEEK; +pub use setavtransportnexturi::SETNEXTAVTRANSPORTURI; +pub use setavtransporturi::SETAVTRANSPORTURI; +pub use stop::STOP; + diff --git a/pmoupnp/src/mediarenderer/avtransport/actions/next.rs b/pmoupnp/src/mediarenderer/avtransport/actions/next.rs new file mode 100644 index 00000000..68118497 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/actions/next.rs @@ -0,0 +1,8 @@ +use crate::mediarenderer::avtransport::variables::A_ARG_TYPE_INSTANCE_ID; +use crate::define_action; + +define_action! { + pub static NEXT = "Next" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + } +} diff --git a/pmoupnp/src/mediarenderer/avtransport/actions/pause.rs b/pmoupnp/src/mediarenderer/avtransport/actions/pause.rs new file mode 100644 index 00000000..0b1f6b98 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/actions/pause.rs @@ -0,0 +1,8 @@ +use crate::mediarenderer::avtransport::variables::A_ARG_TYPE_INSTANCE_ID; +use crate::define_action; + +define_action! { + pub static PAUSE = "Pause" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + } +} diff --git a/pmoupnp/src/mediarenderer/avtransport/actions/play.rs b/pmoupnp/src/mediarenderer/avtransport/actions/play.rs new file mode 100644 index 00000000..5ddc3a1b --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/actions/play.rs @@ -0,0 +1,9 @@ +use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, TRANSPORTPLAYSPEED}; +use crate::define_action; + +define_action! { + pub static PLAY = "Play" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + in "Speed" => TRANSPORTPLAYSPEED, + } +} diff --git a/pmoupnp/src/mediarenderer/avtransport/actions/previous.rs b/pmoupnp/src/mediarenderer/avtransport/actions/previous.rs new file mode 100644 index 00000000..71b1088b --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/actions/previous.rs @@ -0,0 +1,8 @@ +use crate::mediarenderer::avtransport::variables::A_ARG_TYPE_INSTANCE_ID; +use crate::define_action; + +define_action! { + pub static PREVIOUS = "Previous" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + } +} diff --git a/pmoupnp/src/mediarenderer/avtransport/actions/seek.rs b/pmoupnp/src/mediarenderer/avtransport/actions/seek.rs new file mode 100644 index 00000000..e54e7280 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/actions/seek.rs @@ -0,0 +1,10 @@ +use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, A_ARG_TYPE_SEEKMODE, CURRENTTRACKDURATION}; +use crate::define_action; + +define_action! { + pub static SEEK = "Seek" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + in "Unit" => A_ARG_TYPE_SEEKMODE, + in "Target" => CURRENTTRACKDURATION, + } +} diff --git a/pmoupnp/src/mediarenderer/avtransport/actions/setavtransportnexturi.rs b/pmoupnp/src/mediarenderer/avtransport/actions/setavtransportnexturi.rs new file mode 100644 index 00000000..812ce240 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/actions/setavtransportnexturi.rs @@ -0,0 +1,10 @@ +use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA}; +use crate::define_action; + +define_action! { + pub static SETNEXTAVTRANSPORTURI = "SetNextAVTransportURI" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + in "NextURI" => AVTRANSPORTNEXTURI, + in "NextURIMetaData" => AVTRANSPORTNEXTURIMETADATA, + } +} 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..ca4aaeca --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/mod.rs @@ -0,0 +1,156 @@ +//! # AVTransport Service - Service de contrôle de transport UPnP +//! +//! Ce module implémente le service AVTransport:1 selon la spécification UPnP AV. +//! Le service AVTransport permet de contrôler la lecture de médias **audio** +//! sur des rendus multimédias (MediaRenderer Audio). +//! +//! ## Fonctionnalités +//! +//! Le service AVTransport permet : +//! - **Contrôle de lecture** : Play, Pause, Stop +//! - **Navigation** : Next, Previous, Seek +//! - **Gestion des URIs** : SetAVTransportURI, SetNextAVTransportURI +//! - **Information d'état** : GetTransportInfo, GetPositionInfo, GetMediaInfo +//! - **Capacités** : GetDeviceCapabilities, GetCurrentTransportActions +//! +//! ## Conformité UPnP +//! +//! Cette implémentation suit la spécification **UPnP AVTransport:1 Service Template**. +//! Toutes les actions obligatoires (Required) sont implémentées : +//! +//! - ✅ SetAVTransportURI +//! - ✅ GetMediaInfo +//! - ✅ GetTransportInfo +//! - ✅ GetPositionInfo +//! - ✅ GetDeviceCapabilities +//! - ✅ GetTransportSettings +//! - ✅ GetCurrentTransportActions +//! - ✅ Stop, Play, Pause +//! +//! Et certaines actions optionnelles : +//! - ✅ SetNextAVTransportURI +//! - ✅ Seek, Next, Previous +//! +//! ## Variables d'état +//! +//! Le service expose 24 variables d'état conformes à la spécification : +//! +//! ### État du transport +//! - [`TRANSPORTSTATE`] : État actuel (PLAYING, STOPPED, PAUSED_PLAYBACK, etc.) +//! - [`TRANSPORTSTATUS`] : Status du transport (OK, ERROR_OCCURRED) +//! - [`TRANSPORTPLAYSPEED`] : Vitesse de lecture +//! +//! ### Information sur les pistes +//! - [`CURRENTTRACK`] : Numéro de la piste actuelle +//! - [`NUMBEROFTRACKS`] : Nombre total de pistes +//! - [`CURRENTTRACKDURATION`] : Durée de la piste actuelle +//! - [`CURRENTTRACKURI`] : URI de la piste actuelle +//! - [`CURRENTTRACKMETADATA`] : Métadonnées de la piste +//! +//! ### Positionnement +//! - [`RELATIVETIMEPOSITION`] : Position relative dans la piste +//! - [`ABSOLUTETIMEPOSITION`] : Position absolue +//! +//! ### URIs et métadonnées +//! - [`AVTRANSPORTURI`] : URI de la ressource en cours +//! - [`AVTRANSPORTURIMETADATA`] : Métadonnées associées +//! - [`AVTRANSPORTNEXTURI`] : URI de la ressource suivante +//! - [`AVTRANSPORTNEXTURIMETADATA`] : Métadonnées de la ressource suivante +//! +//! ### Modes et capacités +//! - [`CURRENTPLAYMODE`] : Mode de lecture (NORMAL, SHUFFLE, REPEAT_ONE, etc.) +//! - [`PLAYBACKSTORAGEMEDIUM`] : Support de lecture (NETWORK, HDD, CD-DA, etc.) +//! - [`POSSIBLEPLAYBACKSTORAGEMEDIA`] : Supports de lecture possibles +//! +//! ## Examples +//! +//! ```rust +//! use pmoupnp::mediarenderer::avtransport::AVTTRANSPORT; +//! +//! // Accéder au service +//! let service = &*AVTTRANSPORT; +//! println!("Service: {}", service.name()); +//! println!("Type: {}", service.service_type()); +//! +//! // Lister les actions disponibles +//! for action in service.actions() { +//! println!(" Action: {}", action.get_name()); +//! } +//! +//! // Lister les variables d'état +//! for variable in service.variables() { +//! println!(" Variable: {}", variable.get_name()); +//! } +//! ``` +//! +//! ## Références +//! +//! - [UPnP AVTransport:1 Service Template](https://www.upnp.org/specs/av/UPnP-av-AVTransport-v1-Service.pdf) +//! - [UPnP AV Architecture](https://upnp.org/specs/av/) + +use crate::define_service; + +pub mod variables; +pub mod actions; + +use actions::{ + GETCURRENTTRANSPORTACTIONS, GETDEVICECAPABILITIES, GETMEDIAINFO, + GETPOSITIONINFO, GETTRANSPORTINFO, GETTRANSPORTSETTINGS, NEXT, PAUSE, + PLAY, PREVIOUS, SEEK, SETNEXTAVTRANSPORTURI, SETAVTRANSPORTURI, STOP +}; +use variables::{ + ABSOLUTETIMEPOSITION, AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA, + AVTRANSPORTURI, AVTRANSPORTURIMETADATA, A_ARG_TYPE_INSTANCE_ID, + A_ARG_TYPE_PLAY_SPEED, A_ARG_TYPE_SEEKMODE, CURRENTMEDIADURATION, + CURRENTPLAYMODE, CURRENTTRACK, CURRENTTRACKDURATION, CURRENTTRACKMETADATA, + CURRENTTRACKURI, NUMBEROFTRACKS, PLAYBACKSTORAGEMEDIUM, + POSSIBLEPLAYBACKSTORAGEMEDIA, RELATIVETIMEPOSITION, SEEKMODE, + TRANSPORTPLAYSPEED, TRANSPORTSTATE, TRANSPORTSTATUS +}; + +// Service AVTransport:1 conforme à la spécification UPnP AV pour MediaRenderer audio +// Voir la documentation du module pour plus de détails +define_service! { + pub static AVTTRANSPORT = "AVTransport" { + variables: [ + ABSOLUTETIMEPOSITION, + A_ARG_TYPE_INSTANCE_ID, + A_ARG_TYPE_PLAY_SPEED, + A_ARG_TYPE_SEEKMODE, + AVTRANSPORTNEXTURI, + AVTRANSPORTNEXTURIMETADATA, + AVTRANSPORTURI, + AVTRANSPORTURIMETADATA, + CURRENTMEDIADURATION, + CURRENTPLAYMODE, + CURRENTTRACK, + CURRENTTRACKDURATION, + CURRENTTRACKMETADATA, + CURRENTTRACKURI, + NUMBEROFTRACKS, + PLAYBACKSTORAGEMEDIUM, + POSSIBLEPLAYBACKSTORAGEMEDIA, + RELATIVETIMEPOSITION, + SEEKMODE, + TRANSPORTPLAYSPEED, + TRANSPORTSTATE, + TRANSPORTSTATUS, + ], + actions: [ + GETCURRENTTRANSPORTACTIONS, + GETDEVICECAPABILITIES, + GETMEDIAINFO, + GETPOSITIONINFO, + GETTRANSPORTINFO, + GETTRANSPORTSETTINGS, + NEXT, + PAUSE, + PLAY, + PREVIOUS, + SEEK, + SETNEXTAVTRANSPORTURI, + SETAVTRANSPORTURI, + STOP, + ] + } +} \ No newline at end of file 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..264b1f91 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_instanceid.rs @@ -0,0 +1,5 @@ +use crate::define_variable; + +define_variable! { + pub static A_ARG_TYPE_INSTANCE_ID: UI4 = "A_ARG_TYPE_InstanceID" +} 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..d0c54e54 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_playspeed.rs @@ -0,0 +1,5 @@ +use crate::define_variable; + +define_variable! { + pub static A_ARG_TYPE_PLAY_SPEED: String = "A_ARG_TYPE_PlaySpeed" +} diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_seekmode.rs b/pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_seekmode.rs new file mode 100644 index 00000000..edb0619c --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_seekmode.rs @@ -0,0 +1,7 @@ +use crate::define_variable; + +define_variable! { + pub static A_ARG_TYPE_SEEKMODE: String = "A_ARG_TYPE_SeekMode" { + allowed: ["TRACK_NR", "REL_TIME", "ABS_TIME"], + } +} diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/avtransporturi.rs b/pmoupnp/src/mediarenderer/avtransport/variables/avtransporturi.rs new file mode 100644 index 00000000..819eeb1d --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/avtransporturi.rs @@ -0,0 +1,9 @@ +use crate::define_variable; + +define_variable! { + pub static AVTRANSPORTURI: String = "AVTransportURI" +} + +define_variable! { + pub static AVTRANSPORTNEXTURI: String = "AVTransportNextURI" +} diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/avtransporturimetadata.rs b/pmoupnp/src/mediarenderer/avtransport/variables/avtransporturimetadata.rs new file mode 100644 index 00000000..1acd202e --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/avtransporturimetadata.rs @@ -0,0 +1,31 @@ +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}; + + +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(|| -> Arc { + let mut sv = StateVariable::new(StateVarType::String, "AVTransportURIMetaData".to_string()); + + sv.set_value_parser(Arc::new(avtransporturimetadataparser)).expect("Failed to set parser"); + Arc::new(sv) +}); + +pub static AVTRANSPORTNEXTURIMETADATA: Lazy> = Lazy::new(|| -> Arc { + let mut sv = StateVariable::new(StateVarType::String, "AVTransportNextURIMetaData".to_string()); + + sv.set_value_parser(Arc::new(avtransporturimetadataparser)).expect("Failed to set parser"); + Arc::new(sv) +}); diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/currentmediaduration.rs b/pmoupnp/src/mediarenderer/avtransport/variables/currentmediaduration.rs new file mode 100644 index 00000000..83a38698 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/currentmediaduration.rs @@ -0,0 +1,5 @@ +use crate::define_variable; + +define_variable! { + pub static CURRENTMEDIADURATION: String = "CurrentMediaDuration" +} diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/currentplaymode.rs b/pmoupnp/src/mediarenderer/avtransport/variables/currentplaymode.rs new file mode 100644 index 00000000..1a2191e5 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/currentplaymode.rs @@ -0,0 +1,8 @@ +use crate::define_variable; + +define_variable! { + pub static CURRENTPLAYMODE: String = "CurrentPlayMode" { + allowed: ["NORMAL", "SHUFFLE", "REPEAT_ONE", "REPEAT_ALL", "RANDOM", "DIRECT_1", "INTRO"], + default: "NORMAL", + } +} diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/currenttrackmetadata.rs b/pmoupnp/src/mediarenderer/avtransport/variables/currenttrackmetadata.rs new file mode 100644 index 00000000..0e58ea55 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/currenttrackmetadata.rs @@ -0,0 +1,5 @@ +use crate::define_variable; + +define_variable! { + pub static CURRENTTRACKMETADATA: String = "CurrentTrackMetaData" +} diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/currenttrackuri.rs b/pmoupnp/src/mediarenderer/avtransport/variables/currenttrackuri.rs new file mode 100644 index 00000000..fa77dc0e --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/currenttrackuri.rs @@ -0,0 +1,5 @@ +use crate::define_variable; + +define_variable! { + pub static CURRENTTRACKURI: String = "CurrentTrackURI" +} diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/mod.rs b/pmoupnp/src/mediarenderer/avtransport/variables/mod.rs new file mode 100644 index 00000000..00cf43fd --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/mod.rs @@ -0,0 +1,47 @@ +mod a_arg_type_instanceid; +mod a_arg_type_playspeed; +mod a_arg_type_seekmode; +mod avtransporturi; +mod avtransporturimetadata; +mod currentmediaduration; +mod currentplaymode; +mod currenttrackmetadata; +mod currenttrackuri; +mod playbackstoragemedium; +mod possibleplaybackstoragemedia; +mod possiblerecordstoragemedia; +mod recordstoragemedium; +mod seekmode; +mod track; +mod trackduration; +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 a_arg_type_seekmode::A_ARG_TYPE_SEEKMODE; +pub use avtransporturi::AVTRANSPORTURI; +pub use avtransporturi::AVTRANSPORTNEXTURI; +pub use avtransporturimetadata::AVTRANSPORTURIMETADATA; +pub use avtransporturimetadata::AVTRANSPORTNEXTURIMETADATA; +pub use currentmediaduration::CURRENTMEDIADURATION; +pub use currentplaymode::CURRENTPLAYMODE; +pub use currenttrackmetadata::CURRENTTRACKMETADATA; +pub use currenttrackuri::CURRENTTRACKURI; +pub use playbackstoragemedium::PLAYBACKSTORAGEMEDIUM; +pub use possibleplaybackstoragemedia::POSSIBLEPLAYBACKSTORAGEMEDIA; +pub use possiblerecordstoragemedia::POSSIBLERECORDSTORAGEMEDIA; +pub use recordstoragemedium::RECORDSTORAGEMEDIUM; +pub use seekmode::SEEKMODE; +pub use track::CURRENTTRACK; +pub use track::NUMBEROFTRACKS; +pub use trackduration::ABSOLUTETIMEPOSITION; +pub use trackduration::CURRENTTRACKDURATION; +pub use trackduration::RELATIVETIMEPOSITION; +pub use transportplayspeed::TRANSPORTPLAYSPEED; +pub use transportstate::TRANSPORTSTATE; +pub use transportstatus::TRANSPORTSTATUS; + + + diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/playbackstoragemedium.rs b/pmoupnp/src/mediarenderer/avtransport/variables/playbackstoragemedium.rs new file mode 100644 index 00000000..83c3434c --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/playbackstoragemedium.rs @@ -0,0 +1,12 @@ +use crate::define_variable; + +// Valeurs pour un MediaRenderer audio uniquement (suppression des formats vidéo) +define_variable! { + pub static PLAYBACKSTORAGEMEDIUM: String = "PlaybackStorageMedium" { + allowed: [ + "UNKNOWN", "CD-ROM", "CD-DA", "CD-R", "CD-RW", "SACD", + "MD-AUDIO", "DVD-AUDIO", "DAT", "HDD", "NETWORK", + "NONE", "NOT_IMPLEMENTED" + ], + } +} diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/possibleplaybackstoragemedia.rs b/pmoupnp/src/mediarenderer/avtransport/variables/possibleplaybackstoragemedia.rs new file mode 100644 index 00000000..5e7eb8b5 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/possibleplaybackstoragemedia.rs @@ -0,0 +1,5 @@ +use crate::define_variable; + +define_variable! { + pub static POSSIBLEPLAYBACKSTORAGEMEDIA: String = "PossiblePlaybackStorageMedia" +} diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/possiblerecordstoragemedia.rs b/pmoupnp/src/mediarenderer/avtransport/variables/possiblerecordstoragemedia.rs new file mode 100644 index 00000000..2d0ef55c --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/possiblerecordstoragemedia.rs @@ -0,0 +1,10 @@ +use std::sync::Arc; + +use crate::state_variables::StateVariable; +use crate::variable_types::StateVarType; +use once_cell::sync::Lazy; + +pub static POSSIBLERECORDSTORAGEMEDIA: Lazy> = Lazy::new(|| -> Arc { + let sv = StateVariable::new(StateVarType::String, "PossibleRecordStorageMedia".to_string()); + Arc::new(sv) +}); diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/recordstoragemedium.rs b/pmoupnp/src/mediarenderer/avtransport/variables/recordstoragemedium.rs new file mode 100644 index 00000000..19a42660 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/recordstoragemedium.rs @@ -0,0 +1,10 @@ +use std::sync::Arc; + +use crate::state_variables::StateVariable; +use crate::variable_types::StateVarType; +use once_cell::sync::Lazy; + +pub static RECORDSTORAGEMEDIUM: Lazy> = Lazy::new(|| -> Arc { + let sv = StateVariable::new(StateVarType::String, "RecordStorageMedium".to_string()); + Arc::new(sv) +}); diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/seekmode.rs b/pmoupnp/src/mediarenderer/avtransport/variables/seekmode.rs new file mode 100644 index 00000000..54970ac6 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/seekmode.rs @@ -0,0 +1,6 @@ +use crate::define_variable; + +define_variable! { + pub static SEEKMODE: String = "SeekMode" +} + diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/track.rs b/pmoupnp/src/mediarenderer/avtransport/variables/track.rs new file mode 100644 index 00000000..b0c0696f --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/track.rs @@ -0,0 +1,14 @@ +use crate::define_variable; + +define_variable! { + pub static CURRENTTRACK: String = "CurrentTrack" { + evented: true, + } +} + +define_variable! { + pub static NUMBEROFTRACKS: String = "NumberOfTracks" { + evented: true, + } +} + diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/trackduration.rs b/pmoupnp/src/mediarenderer/avtransport/variables/trackduration.rs new file mode 100644 index 00000000..30797722 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/trackduration.rs @@ -0,0 +1,20 @@ +use crate::define_variable; + +define_variable! { + pub static CURRENTTRACKDURATION: String = "CurrentTrackDuration" { + evented: true, + } +} + +define_variable! { + pub static ABSOLUTETIMEPOSITION: String = "AbsoluteTimePosition" { + evented: true, + } +} + +define_variable! { + pub static RELATIVETIMEPOSITION: String = "RelativeTimePosition" { + evented: true, + } +} + diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/transportplayspeed.rs b/pmoupnp/src/mediarenderer/avtransport/variables/transportplayspeed.rs new file mode 100644 index 00000000..24bd493b --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/transportplayspeed.rs @@ -0,0 +1,9 @@ +use crate::define_variable; + +define_variable! { + pub static TRANSPORTPLAYSPEED: String = "TransportPlaySpeed" { + allowed: ["1"], + default: "1", + } +} + diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/transportstate.rs b/pmoupnp/src/mediarenderer/avtransport/variables/transportstate.rs new file mode 100644 index 00000000..c99f0a3c --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/transportstate.rs @@ -0,0 +1,10 @@ +use crate::define_variable; + +// États pour un MediaRenderer audio uniquement (suppression des états d'enregistrement) +define_variable! { + pub static TRANSPORTSTATE: String = "TransportState" { + allowed: ["STOPPED", "PLAYING", "TRANSITIONING", "PAUSED_PLAYBACK", "NO_MEDIA_PRESENT"], + evented: true, + } +} + diff --git a/pmoupnp/src/mediarenderer/avtransport/variables/transportstatus.rs b/pmoupnp/src/mediarenderer/avtransport/variables/transportstatus.rs new file mode 100644 index 00000000..851a59e3 --- /dev/null +++ b/pmoupnp/src/mediarenderer/avtransport/variables/transportstatus.rs @@ -0,0 +1,8 @@ +use crate::define_variable; + +define_variable! { + pub static TRANSPORTSTATUS: String = "TransportStatus" { + allowed: ["OK", "ERROR_OCCURRED"], + evented: true, + } +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/actions/getcurrentconnectionids.rs b/pmoupnp/src/mediarenderer/connectionmanager/actions/getcurrentconnectionids.rs new file mode 100644 index 00000000..34480ad8 --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/actions/getcurrentconnectionids.rs @@ -0,0 +1,8 @@ +use crate::mediarenderer::connectionmanager::variables::CURRENTCONNECTIONIDS; +use crate::define_action; + +define_action! { + pub static GETCURRENTCONNECTIONIDS = "GetCurrentConnectionIDs" { + out "ConnectionIDs" => CURRENTCONNECTIONIDS, + } +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/actions/getcurrentconnectioninfo.rs b/pmoupnp/src/mediarenderer/connectionmanager/actions/getcurrentconnectioninfo.rs new file mode 100644 index 00000000..e9d4abb0 --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/actions/getcurrentconnectioninfo.rs @@ -0,0 +1,18 @@ +use crate::mediarenderer::connectionmanager::variables::{ + A_ARG_TYPE_CONNECTIONID, A_ARG_TYPE_RCSID, A_ARG_TYPE_AVTRANSPORTID, + A_ARG_TYPE_PROTOCOLINFO, A_ARG_TYPE_DIRECTION, A_ARG_TYPE_CONNECTIONSTATUS +}; +use crate::define_action; + +define_action! { + pub static GETCURRENTCONNECTIONINFO = "GetCurrentConnectionInfo" { + in "ConnectionID" => A_ARG_TYPE_CONNECTIONID, + out "RcsID" => A_ARG_TYPE_RCSID, + out "AVTransportID" => A_ARG_TYPE_AVTRANSPORTID, + out "ProtocolInfo" => A_ARG_TYPE_PROTOCOLINFO, + out "PeerConnectionManager" => A_ARG_TYPE_PROTOCOLINFO, + out "PeerConnectionID" => A_ARG_TYPE_CONNECTIONID, + out "Direction" => A_ARG_TYPE_DIRECTION, + out "Status" => A_ARG_TYPE_CONNECTIONSTATUS, + } +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/actions/getprotocolinfo.rs b/pmoupnp/src/mediarenderer/connectionmanager/actions/getprotocolinfo.rs new file mode 100644 index 00000000..f4207679 --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/actions/getprotocolinfo.rs @@ -0,0 +1,9 @@ +use crate::mediarenderer::connectionmanager::variables::{SOURCEPROTOCOLINFO, SINKPROTOCOLINFO}; +use crate::define_action; + +define_action! { + pub static GETPROTOCOLINFO = "GetProtocolInfo" { + out "Source" => SOURCEPROTOCOLINFO, + out "Sink" => SINKPROTOCOLINFO, + } +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/actions/mod.rs b/pmoupnp/src/mediarenderer/connectionmanager/actions/mod.rs new file mode 100644 index 00000000..b9ccd37b --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/actions/mod.rs @@ -0,0 +1,7 @@ +mod getprotocolinfo; +mod getcurrentconnectionids; +mod getcurrentconnectioninfo; + +pub use getprotocolinfo::GETPROTOCOLINFO; +pub use getcurrentconnectionids::GETCURRENTCONNECTIONIDS; +pub use getcurrentconnectioninfo::GETCURRENTCONNECTIONINFO; diff --git a/pmoupnp/src/mediarenderer/connectionmanager/mod.rs b/pmoupnp/src/mediarenderer/connectionmanager/mod.rs new file mode 100644 index 00000000..7ca0664d --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/mod.rs @@ -0,0 +1,89 @@ +//! # ConnectionManager Service - Service de gestion des connexions UPnP +//! +//! Ce module implémente le service ConnectionManager:1 selon la spécification UPnP AV. +//! Le service ConnectionManager gère les connexions entre MediaServer et MediaRenderer, +//! et expose les protocoles et formats supportés. +//! +//! ## Fonctionnalités +//! +//! Le service ConnectionManager permet : +//! - **Énumération des protocoles** : GetProtocolInfo +//! - **Gestion des connexions** : GetCurrentConnectionIDs, GetCurrentConnectionInfo +//! - Support des formats audio (MP3, FLAC, WAV, etc.) +//! +//! ## Conformité UPnP +//! +//! Cette implémentation suit la spécification **UPnP ConnectionManager:1 Service Template**. +//! Toutes les actions obligatoires (Required) sont implémentées : +//! +//! - ✅ GetProtocolInfo +//! - ✅ GetCurrentConnectionIDs +//! - ✅ GetCurrentConnectionInfo +//! +//! ## Variables d'état +//! +//! Le service expose les variables d'état conformes à la spécification : +//! +//! ### Informations de protocole +//! - [`SOURCEPROTOCOLINFO`] : Protocoles source (vide pour un renderer) +//! - [`SINKPROTOCOLINFO`] : Protocoles sink supportés (http-get:*:audio/mpeg:*, etc.) +//! - [`CURRENTCONNECTIONIDS`] : IDs des connexions actives +//! +//! ### Arguments +//! - [`A_ARG_TYPE_CONNECTIONID`] : ID de connexion +//! - [`A_ARG_TYPE_CONNECTIONSTATUS`] : Statut de connexion +//! - [`A_ARG_TYPE_DIRECTION`] : Direction (Input/Output) +//! - [`A_ARG_TYPE_PROTOCOLINFO`] : Information de protocole +//! - [`A_ARG_TYPE_RCSID`] : ID RenderingControl +//! - [`A_ARG_TYPE_AVTRANSPORTID`] : ID AVTransport +//! +//! ## Examples +//! +//! ```rust +//! use pmoupnp::mediarenderer::connectionmanager::CONNECTIONMANAGER; +//! +//! // Accéder au service +//! let service = &*CONNECTIONMANAGER; +//! println!("Service: {}", service.name()); +//! println!("Type: {}", service.service_type()); +//! ``` +//! +//! ## Références +//! +//! - [UPnP ConnectionManager:1 Service Template](https://upnp.org/specs/av/UPnP-av-ConnectionManager-v1-Service.pdf) +//! - [UPnP AV Architecture](https://upnp.org/specs/av/) + +use crate::define_service; + +pub mod variables; +pub mod actions; + +use actions::{GETCURRENTCONNECTIONIDS, GETCURRENTCONNECTIONINFO, GETPROTOCOLINFO}; +use variables::{ + A_ARG_TYPE_AVTRANSPORTID, A_ARG_TYPE_CONNECTIONID, A_ARG_TYPE_CONNECTIONSTATUS, + A_ARG_TYPE_DIRECTION, A_ARG_TYPE_PROTOCOLINFO, A_ARG_TYPE_RCSID, + CURRENTCONNECTIONIDS, SINKPROTOCOLINFO, SOURCEPROTOCOLINFO +}; + +// Service ConnectionManager:1 conforme à la spécification UPnP AV pour MediaRenderer audio +// Voir la documentation du module pour plus de détails +define_service! { + pub static CONNECTIONMANAGER = "ConnectionManager" { + variables: [ + A_ARG_TYPE_AVTRANSPORTID, + A_ARG_TYPE_CONNECTIONID, + A_ARG_TYPE_CONNECTIONSTATUS, + A_ARG_TYPE_DIRECTION, + A_ARG_TYPE_PROTOCOLINFO, + A_ARG_TYPE_RCSID, + CURRENTCONNECTIONIDS, + SINKPROTOCOLINFO, + SOURCEPROTOCOLINFO, + ], + actions: [ + GETCURRENTCONNECTIONIDS, + GETCURRENTCONNECTIONINFO, + GETPROTOCOLINFO, + ] + } +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_avtransportid.rs b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_avtransportid.rs new file mode 100644 index 00000000..5445904c --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_avtransportid.rs @@ -0,0 +1,5 @@ +use crate::define_variable; + +define_variable! { + pub static A_ARG_TYPE_AVTRANSPORTID: I4 = "A_ARG_TYPE_AVTransportID" +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_connectionid.rs b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_connectionid.rs new file mode 100644 index 00000000..6349d6b3 --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_connectionid.rs @@ -0,0 +1,5 @@ +use crate::define_variable; + +define_variable! { + pub static A_ARG_TYPE_CONNECTIONID: I4 = "A_ARG_TYPE_ConnectionID" +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_connectionstatus.rs b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_connectionstatus.rs new file mode 100644 index 00000000..402a7736 --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_connectionstatus.rs @@ -0,0 +1,7 @@ +use crate::define_variable; + +define_variable! { + pub static A_ARG_TYPE_CONNECTIONSTATUS: String = "A_ARG_TYPE_ConnectionStatus" { + allowed: ["OK", "ContentFormatMismatch", "InsufficientBandwidth", "UnreliableChannel", "Unknown"], + } +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_direction.rs b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_direction.rs new file mode 100644 index 00000000..8604577b --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_direction.rs @@ -0,0 +1,7 @@ +use crate::define_variable; + +define_variable! { + pub static A_ARG_TYPE_DIRECTION: String = "A_ARG_TYPE_Direction" { + allowed: ["Input", "Output"], + } +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_protocolinfo.rs b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_protocolinfo.rs new file mode 100644 index 00000000..1d19b80c --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_protocolinfo.rs @@ -0,0 +1,5 @@ +use crate::define_variable; + +define_variable! { + pub static A_ARG_TYPE_PROTOCOLINFO: String = "A_ARG_TYPE_ProtocolInfo" +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_rcsid.rs b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_rcsid.rs new file mode 100644 index 00000000..ba902a0a --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_rcsid.rs @@ -0,0 +1,5 @@ +use crate::define_variable; + +define_variable! { + pub static A_ARG_TYPE_RCSID: I4 = "A_ARG_TYPE_RcsID" +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/variables/currentconnectionids.rs b/pmoupnp/src/mediarenderer/connectionmanager/variables/currentconnectionids.rs new file mode 100644 index 00000000..afe28709 --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/variables/currentconnectionids.rs @@ -0,0 +1,7 @@ +use crate::define_variable; + +define_variable! { + pub static CURRENTCONNECTIONIDS: String = "CurrentConnectionIDs" { + evented: true, + } +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/variables/mod.rs b/pmoupnp/src/mediarenderer/connectionmanager/variables/mod.rs new file mode 100644 index 00000000..4bc47ea4 --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/variables/mod.rs @@ -0,0 +1,19 @@ +mod sourceprotocolinfo; +mod sinkprotocolinfo; +mod currentconnectionids; +mod a_arg_type_connectionid; +mod a_arg_type_connectionstatus; +mod a_arg_type_direction; +mod a_arg_type_protocolinfo; +mod a_arg_type_rcsid; +mod a_arg_type_avtransportid; + +pub use sourceprotocolinfo::SOURCEPROTOCOLINFO; +pub use sinkprotocolinfo::SINKPROTOCOLINFO; +pub use currentconnectionids::CURRENTCONNECTIONIDS; +pub use a_arg_type_connectionid::A_ARG_TYPE_CONNECTIONID; +pub use a_arg_type_connectionstatus::A_ARG_TYPE_CONNECTIONSTATUS; +pub use a_arg_type_direction::A_ARG_TYPE_DIRECTION; +pub use a_arg_type_protocolinfo::A_ARG_TYPE_PROTOCOLINFO; +pub use a_arg_type_rcsid::A_ARG_TYPE_RCSID; +pub use a_arg_type_avtransportid::A_ARG_TYPE_AVTRANSPORTID; diff --git a/pmoupnp/src/mediarenderer/connectionmanager/variables/sinkprotocolinfo.rs b/pmoupnp/src/mediarenderer/connectionmanager/variables/sinkprotocolinfo.rs new file mode 100644 index 00000000..5022c92a --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/variables/sinkprotocolinfo.rs @@ -0,0 +1,8 @@ +use crate::define_variable; + +// Pour un MediaRenderer audio, liste les protocoles/formats audio supportés +define_variable! { + pub static SINKPROTOCOLINFO: String = "SinkProtocolInfo" { + evented: true, + } +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/variables/sourceprotocolinfo.rs b/pmoupnp/src/mediarenderer/connectionmanager/variables/sourceprotocolinfo.rs new file mode 100644 index 00000000..f182d9ac --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/variables/sourceprotocolinfo.rs @@ -0,0 +1,7 @@ +use crate::define_variable; + +define_variable! { + pub static SOURCEPROTOCOLINFO: String = "SourceProtocolInfo" { + evented: true, + } +} diff --git a/pmoupnp/src/mediarenderer/mod.rs b/pmoupnp/src/mediarenderer/mod.rs new file mode 100644 index 00000000..7f8e06b6 --- /dev/null +++ b/pmoupnp/src/mediarenderer/mod.rs @@ -0,0 +1,3 @@ +pub mod avtransport; +pub mod connectionmanager; +pub mod renderingcontrol; diff --git a/pmoupnp/src/mediarenderer/renderingcontrol/actions/getmute.rs b/pmoupnp/src/mediarenderer/renderingcontrol/actions/getmute.rs new file mode 100644 index 00000000..f39e1e2c --- /dev/null +++ b/pmoupnp/src/mediarenderer/renderingcontrol/actions/getmute.rs @@ -0,0 +1,10 @@ +use crate::mediarenderer::renderingcontrol::variables::{A_ARG_TYPE_INSTANCE_ID, A_ARG_TYPE_CHANNEL, MUTE}; +use crate::define_action; + +define_action! { + pub static GETMUTE = "GetMute" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + in "Channel" => A_ARG_TYPE_CHANNEL, + out "CurrentMute" => MUTE, + } +} diff --git a/pmoupnp/src/mediarenderer/renderingcontrol/actions/getvolume.rs b/pmoupnp/src/mediarenderer/renderingcontrol/actions/getvolume.rs new file mode 100644 index 00000000..c6610618 --- /dev/null +++ b/pmoupnp/src/mediarenderer/renderingcontrol/actions/getvolume.rs @@ -0,0 +1,10 @@ +use crate::mediarenderer::renderingcontrol::variables::{A_ARG_TYPE_INSTANCE_ID, A_ARG_TYPE_CHANNEL, VOLUME}; +use crate::define_action; + +define_action! { + pub static GETVOLUME = "GetVolume" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + in "Channel" => A_ARG_TYPE_CHANNEL, + out "CurrentVolume" => VOLUME, + } +} diff --git a/pmoupnp/src/mediarenderer/renderingcontrol/actions/mod.rs b/pmoupnp/src/mediarenderer/renderingcontrol/actions/mod.rs new file mode 100644 index 00000000..b7998a94 --- /dev/null +++ b/pmoupnp/src/mediarenderer/renderingcontrol/actions/mod.rs @@ -0,0 +1,9 @@ +mod getvolume; +mod setvolume; +mod getmute; +mod setmute; + +pub use getvolume::GETVOLUME; +pub use setvolume::SETVOLUME; +pub use getmute::GETMUTE; +pub use setmute::SETMUTE; diff --git a/pmoupnp/src/mediarenderer/renderingcontrol/actions/setmute.rs b/pmoupnp/src/mediarenderer/renderingcontrol/actions/setmute.rs new file mode 100644 index 00000000..5220f00f --- /dev/null +++ b/pmoupnp/src/mediarenderer/renderingcontrol/actions/setmute.rs @@ -0,0 +1,10 @@ +use crate::mediarenderer::renderingcontrol::variables::{A_ARG_TYPE_INSTANCE_ID, A_ARG_TYPE_CHANNEL, MUTE}; +use crate::define_action; + +define_action! { + pub static SETMUTE = "SetMute" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + in "Channel" => A_ARG_TYPE_CHANNEL, + in "DesiredMute" => MUTE, + } +} diff --git a/pmoupnp/src/mediarenderer/renderingcontrol/actions/setvolume.rs b/pmoupnp/src/mediarenderer/renderingcontrol/actions/setvolume.rs new file mode 100644 index 00000000..d403fa45 --- /dev/null +++ b/pmoupnp/src/mediarenderer/renderingcontrol/actions/setvolume.rs @@ -0,0 +1,10 @@ +use crate::mediarenderer::renderingcontrol::variables::{A_ARG_TYPE_INSTANCE_ID, A_ARG_TYPE_CHANNEL, VOLUME}; +use crate::define_action; + +define_action! { + pub static SETVOLUME = "SetVolume" { + in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, + in "Channel" => A_ARG_TYPE_CHANNEL, + in "DesiredVolume" => VOLUME, + } +} diff --git a/pmoupnp/src/mediarenderer/renderingcontrol/mod.rs b/pmoupnp/src/mediarenderer/renderingcontrol/mod.rs new file mode 100644 index 00000000..5d711521 --- /dev/null +++ b/pmoupnp/src/mediarenderer/renderingcontrol/mod.rs @@ -0,0 +1,77 @@ +//! # RenderingControl Service - Service de contrôle du rendu audio UPnP +//! +//! Ce module implémente le service RenderingControl:1 selon la spécification UPnP AV. +//! Le service RenderingControl permet de contrôler les paramètres de rendu audio +//! (volume, mute, etc.) sur un MediaRenderer. +//! +//! ## Fonctionnalités +//! +//! Le service RenderingControl permet : +//! - **Contrôle du volume** : GetVolume, SetVolume +//! - **Contrôle du mute** : GetMute, SetMute +//! - Support multi-canal (Master, LF, RF) +//! +//! ## Conformité UPnP +//! +//! Cette implémentation suit la spécification **UPnP RenderingControl:1 Service Template**. +//! Toutes les actions obligatoires (Required) sont implémentées : +//! +//! - ✅ GetVolume +//! - ✅ SetVolume +//! - ✅ GetMute +//! - ✅ SetMute +//! +//! ## Variables d'état +//! +//! Le service expose les variables d'état conformes à la spécification : +//! +//! ### Contrôle audio +//! - [`VOLUME`] : Niveau de volume (0-100) +//! - [`MUTE`] : État mute (true/false) +//! +//! ### Arguments +//! - [`A_ARG_TYPE_INSTANCE_ID`] : ID d'instance +//! - [`A_ARG_TYPE_CHANNEL`] : Canal audio (Master, LF, RF) +//! +//! ## Examples +//! +//! ```rust +//! use pmoupnp::mediarenderer::renderingcontrol::RENDERINGCONTROL; +//! +//! // Accéder au service +//! let service = &*RENDERINGCONTROL; +//! println!("Service: {}", service.name()); +//! println!("Type: {}", service.service_type()); +//! ``` +//! +//! ## Références +//! +//! - [UPnP RenderingControl:1 Service Template](https://upnp.org/specs/av/UPnP-av-RenderingControl-v1-Service.pdf) +//! - [UPnP AV Architecture](https://upnp.org/specs/av/) + +use crate::define_service; + +pub mod variables; +pub mod actions; + +use actions::{GETMUTE, GETVOLUME, SETMUTE, SETVOLUME}; +use variables::{A_ARG_TYPE_CHANNEL, A_ARG_TYPE_INSTANCE_ID, MUTE, VOLUME}; + +// Service RenderingControl:1 conforme à la spécification UPnP AV pour MediaRenderer audio +// Voir la documentation du module pour plus de détails +define_service! { + pub static RENDERINGCONTROL = "RenderingControl" { + variables: [ + A_ARG_TYPE_CHANNEL, + A_ARG_TYPE_INSTANCE_ID, + MUTE, + VOLUME, + ], + actions: [ + GETMUTE, + GETVOLUME, + SETMUTE, + SETVOLUME, + ] + } +} diff --git a/pmoupnp/src/mediarenderer/renderingcontrol/variables/a_arg_type_channel.rs b/pmoupnp/src/mediarenderer/renderingcontrol/variables/a_arg_type_channel.rs new file mode 100644 index 00000000..41d588d7 --- /dev/null +++ b/pmoupnp/src/mediarenderer/renderingcontrol/variables/a_arg_type_channel.rs @@ -0,0 +1,8 @@ +use crate::define_variable; + +define_variable! { + pub static A_ARG_TYPE_CHANNEL: String = "A_ARG_TYPE_Channel" { + allowed: ["Master", "LF", "RF"], + default: "Master", + } +} diff --git a/pmoupnp/src/mediarenderer/renderingcontrol/variables/a_arg_type_instanceid.rs b/pmoupnp/src/mediarenderer/renderingcontrol/variables/a_arg_type_instanceid.rs new file mode 100644 index 00000000..264b1f91 --- /dev/null +++ b/pmoupnp/src/mediarenderer/renderingcontrol/variables/a_arg_type_instanceid.rs @@ -0,0 +1,5 @@ +use crate::define_variable; + +define_variable! { + pub static A_ARG_TYPE_INSTANCE_ID: UI4 = "A_ARG_TYPE_InstanceID" +} diff --git a/pmoupnp/src/mediarenderer/renderingcontrol/variables/mod.rs b/pmoupnp/src/mediarenderer/renderingcontrol/variables/mod.rs new file mode 100644 index 00000000..11a2e2c4 --- /dev/null +++ b/pmoupnp/src/mediarenderer/renderingcontrol/variables/mod.rs @@ -0,0 +1,9 @@ +mod a_arg_type_instanceid; +mod a_arg_type_channel; +mod volume; +mod mute; + +pub use a_arg_type_instanceid::A_ARG_TYPE_INSTANCE_ID; +pub use a_arg_type_channel::A_ARG_TYPE_CHANNEL; +pub use volume::VOLUME; +pub use mute::MUTE; diff --git a/pmoupnp/src/mediarenderer/renderingcontrol/variables/mute.rs b/pmoupnp/src/mediarenderer/renderingcontrol/variables/mute.rs new file mode 100644 index 00000000..bbae7f0d --- /dev/null +++ b/pmoupnp/src/mediarenderer/renderingcontrol/variables/mute.rs @@ -0,0 +1,7 @@ +use crate::define_variable; + +define_variable! { + pub static MUTE: Boolean = "Mute" { + evented: true, + } +} diff --git a/pmoupnp/src/mediarenderer/renderingcontrol/variables/volume.rs b/pmoupnp/src/mediarenderer/renderingcontrol/variables/volume.rs new file mode 100644 index 00000000..f59ee701 --- /dev/null +++ b/pmoupnp/src/mediarenderer/renderingcontrol/variables/volume.rs @@ -0,0 +1,7 @@ +use crate::define_variable; + +define_variable! { + pub static VOLUME: UI2 = "Volume" { + evented: true, + } +} diff --git a/pmoupnp/src/object_set.rs b/pmoupnp/src/object_set.rs new file mode 100644 index 00000000..224ce185 --- /dev/null +++ b/pmoupnp/src/object_set.rs @@ -0,0 +1,197 @@ +use std::{collections::HashMap, sync::Arc}; + +use std::sync::RwLock; + +use crate::{UpnpDeepClone, UpnpObjectSet, UpnpObjectSetError, UpnpSet, UpnpTypedObject}; + +/// Implémentation du clonage profond pour `UpnpObjectSet`. +/// +/// Cette implémentation crée une copie complète et indépendante du set, +/// en clonant chaque objet `T` et en créant de nouveaux `Arc` autour de ces clones. +/// Les modifications sur l'un des sets n'affectent pas l'autre. +impl UpnpDeepClone for UpnpObjectSet { + fn deep_clone(&self) -> Self { + let guard = self.objects.read().unwrap(); + + let cloned_map: HashMap> = guard + .iter() + .map(|(key, arc)| (key.clone(), Arc::new((**arc).clone()))) + .collect(); + + Self { + objects: RwLock::new(cloned_map), + } + } +} + +/// Implémentation du clonage superficiel pour `UpnpObjectSet`. +/// +/// Cette implémentation crée une copie du set qui **partage** les objets `T` +/// via les `Arc`. C'est beaucoup plus rapide et économe en mémoire qu'un clonage +/// profond, car seuls les pointeurs `Arc` sont clonés (incrémentation du compteur +/// de références). +/// +/// # Note +/// +/// Les deux sets partagent les mêmes instances d'objets `T`. Si `T` contient +/// de la mutabilité interne (via `Mutex`, `RwLock`, etc.), les modifications +/// seront visibles depuis les deux sets. +impl Clone for UpnpObjectSet { + fn clone(&self) -> Self { + let guard = self.objects.read().unwrap(); + + Self { + objects: RwLock::new(guard.clone()), + } + } +} + +impl UpnpObjectSet { + /// Crée un nouveau `UpnpObjectSet` vide. + /// + /// # Examples + /// + /// ``` + /// let set: UpnpObjectSet = UpnpObjectSet::new(); + /// ``` + pub fn new() -> Self { + Self { + objects: RwLock::new(HashMap::new()), + } + } + + /// Insère un objet dans le set. + /// + /// # Arguments + /// + /// * `object` - L'objet à insérer, encapsulé dans un `Arc` + /// + /// # Returns + /// + /// * `Ok(())` - Si l'insertion a réussi + /// * `Err(UpnpObjectSetError::AlreadyExists)` - Si un objet avec le même nom existe déjà + /// + /// # Examples + /// + /// ``` + /// let mut set = UpnpObjectSet::new(); + /// let obj = Arc::new(MyObject::new("test")); + /// set.insert(obj)?; + /// ``` + pub fn insert(&mut self, object: Arc) -> Result<(), UpnpObjectSetError> { + let mut guard = self.objects.write().unwrap(); + let key = object.get_name().to_string(); + + if guard.contains_key(&key) { + return Err(UpnpObjectSetError::AlreadyExists(key)); + } + + guard.insert(key, object); + Ok(()) + } + + /// Insère un objet dans le set, ou remplace l'objet existant s'il y en a un avec le même nom. + /// + /// Cette méthode ne retourne jamais d'erreur et écrase silencieusement tout objet existant. + /// + /// # Arguments + /// + /// * `object` - L'objet à insérer ou remplacer, encapsulé dans un `Arc` + /// + /// # Examples + /// + /// ``` + /// let mut set = UpnpObjectSet::new(); + /// let obj1 = Arc::new(MyObject::new("test")); + /// let obj2 = Arc::new(MyObject::new("test")); // Même nom + /// + /// set.insert_or_replace(obj1); + /// set.insert_or_replace(obj2); // Remplace obj1 + /// ``` + pub fn insert_or_replace(&mut self, object: Arc) { + let mut guard = self.objects.write().unwrap(); + let key: String = object.get_name().to_string(); + + guard.insert(key, object); + } + + /// Vérifie si le set contient un objet donné. + /// + /// La vérification se base sur le nom de l'objet retourné par `get_name()`. + /// + /// # Arguments + /// + /// * `object` - L'objet à rechercher + /// + /// # Returns + /// + /// `true` si un objet avec le même nom existe dans le set, `false` sinon. + /// + /// # Examples + /// + /// ``` + /// let set = UpnpObjectSet::new(); + /// let obj = Arc::new(MyObject::new("test")); + /// + /// if set.contains(obj.clone()) { + /// println!("L'objet existe déjà"); + /// } + /// ``` + pub fn contains(&self, object: Arc) -> bool { + let guard = self.objects.read().unwrap(); + let key: String = object.get_name().to_string(); + + guard.contains_key(&key) + } + + /// Récupère un objet par son nom. + /// + /// # Arguments + /// + /// * `name` - Le nom de l'objet à rechercher + /// + /// # Returns + /// + /// * `Some(Arc)` - Si un objet avec ce nom existe + /// * `None` - Si aucun objet n'est trouvé + /// + /// # Examples + /// + /// ``` + /// let set = UpnpObjectSet::new(); + /// + /// if let Some(obj) = set.get_by_name("test") { + /// println!("Objet trouvé: {}", obj.get_name()); + /// } + /// ``` + pub fn get_by_name(&self, name: &str) -> Option> { + let guard = self.objects.read().unwrap(); + guard.get(name).cloned() + } + + /// Retourne tous les objets du set. + /// + /// # Returns + /// + /// Un vecteur contenant des clones des `Arc` pointant vers tous les objets du set. + /// L'ordre des éléments n'est pas garanti. + /// + /// # Examples + /// + /// ``` + /// let set = UpnpObjectSet::new(); + /// + /// for obj in set.all() { + /// println!("Objet: {}", obj.get_name()); + /// } + /// ``` + /// + /// # Thread-safety + /// + /// Cette méthode acquiert un verrou de lecture. Plusieurs threads peuvent + /// appeler cette méthode simultanément sans blocage. + pub fn all(&self) -> Vec> { + let guard = self.objects.read().unwrap(); + guard.values().cloned().collect() + } +} \ No newline at end of file diff --git a/pmoupnp/src/object_trait.rs b/pmoupnp/src/object_trait.rs new file mode 100644 index 00000000..99c151e9 --- /dev/null +++ b/pmoupnp/src/object_trait.rs @@ -0,0 +1,766 @@ +//! ## Hiérarchie des traits +//! +//! ```text +//! Clone + Debug +//! └─> UpnpObject (trait de base) +//! ├─> UpnpModel (modèles créant des instances) +//! ├─> UpnpInstance (instances concrètes) +//! ├─> UpnpTyped (objets avec nom et type) +//! │ └─> UpnpTypedObject = UpnpObject + UpnpTyped +//! │ └─> UpnpTypedInstance = UpnpTypedObject + UpnpInstance +//! └─> UpnpSet (collections) + UpnpDeepClone +//! ├─> UpnpModelSet = UpnpSet + UpnpModel +//! └─> UpnInstanceSet = UpnpSet + UpnpInstance +//! +//! UpnpDeepClone (indépendant) +//! ``` +//! +//! ## Description des traits +//! +//! - **Traits de base** : +//! - [`UpnpObject`] : Trait principal avec sérialisation XML/Markdown +//! - [`UpnpDeepClone`] : Clonage profond (indépendant de la hiérarchie) +//! +//! - **Traits de spécialisation niveau 1** : +//! - [`UpnpModel`] : Modèle pouvant créer des instances +//! - [`UpnpInstance`] : Instance concrète créée depuis un modèle +//! - [`UpnpTyped`] : Ajoute les informations de type et nom +//! - [`UpnpSet`] : Marque un objet comme collection +//! +//! - **Traits combinés niveau 2** : +//! - [`UpnpTypedObject`] : Objet typé (marker trait) +//! +//! - **Traits combinés niveau 3** : +//! - [`UpnpTypedInstance`] : Instance typée (marker trait) +//! - [`UpnpModelSet`] : Collection de modèles (marker trait) +//! - [`UpnInstanceSet`] : Collection d'instances (marker trait) + +use std::{fmt::Debug, sync::Arc}; + +use xmltree::{Element, EmitterConfig}; + +use crate::UpnpObjectType; + +/// Trait pour le clonage profond d'objets UPnP. +/// +/// Contrairement au trait standard [`Clone`] qui peut effectuer un clonage superficiel +/// (partage via `Arc`), ce trait garantit un clonage complet et indépendant de l'objet. +/// +/// # Note +/// +/// Ce trait est indépendant de la hiérarchie [`UpnpObject`] et peut être implémenté +/// séparément. +pub trait UpnpDeepClone { + /// Crée un clone profond de l'objet. + /// + /// Tous les éléments internes sont clonés, créant un objet complètement indépendant. + fn deep_clone(&self) -> Self; +} + +/// Trait de base pour tous les objets UPnP. +/// +/// Ce trait fournit les fonctionnalités communes à tous les objets UPnP : +/// - Sérialisation XML +/// - Conversion en Markdown +/// - Identification du type d'objet (instance ou set) +/// +/// # Traits requis +/// +/// - [`Clone`] : Pour pouvoir dupliquer les objets +/// - [`Debug`] : Pour le débogage +/// +/// # Hiérarchie +/// +/// Ce trait est à la base de toute la hiérarchie UPnP. Voir la documentation du module +/// pour le graphe complet. +pub trait UpnpObject: Clone + Debug { + /// Convertit l'objet en élément XML. + /// + /// # Returns + /// + /// Un [`Element`] xmltree représentant l'objet. + fn to_xml_element(&self) -> Element; + + /// Convertit l'objet en chaîne XML formatée. + /// + /// Génère une représentation XML complète avec en-tête et indentation. + /// + /// # Returns + /// + /// Une chaîne XML formatée avec : + /// - En-tête `` + /// - Indentation de 2 espaces + /// + /// # Examples + /// + /// ```ignore + /// let xml = my_object.to_xml(); + /// println!("{}", xml); + /// // + /// // + /// // value + /// // + /// ``` + fn to_xml(&self) -> String { + let elem = self.to_xml_element(); + + let config = EmitterConfig::new() + .perform_indent(true) + .indent_string(" "); + + let mut buf = Vec::new(); + elem.write_with_config(&mut buf, config) + .expect("Failed to write XML"); + + let mut xml_string = "\n".to_string(); + xml_string.push_str(&String::from_utf8(buf).expect("Invalid UTF-8")); + + xml_string + } + + /// Convertit l'objet en représentation Markdown. + /// + /// Génère une vue hiérarchique de la structure XML en format Markdown, + /// avec détection automatique des URLs et images. + /// + /// # Fonctionnalités + /// + /// - Les URLs sont converties en liens cliquables + /// - Les URLs d'images sont affichées comme images + /// - Les attributs sont formatés comme `key=value` + /// - Structure hiérarchique avec indentation + /// + /// # Returns + /// + /// Une chaîne Markdown formatée. + /// + /// # Examples + /// + /// ```ignore + /// let md = my_object.to_markdown(); + /// println!("{}", md); + /// // # UPnP XML (Markdown view) + /// // + /// // - **element** + /// // - **child**: `value` + /// ``` + 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(); + 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)); + + 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(", "))); + } + + 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'); + + 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 + } + + /// Indique si l'objet est une instance. + /// + /// # Returns + /// + /// `false` par défaut. Surchargé par [`UpnpInstance`] pour retourner `true`. + fn is_instance(&self) -> bool { + false + } + + /// Indique si l'objet est une collection (set). + /// + /// # Returns + /// + /// `false` par défaut. Surchargé par [`UpnpSet`] pour retourner `true`. + fn is_set(&self) -> bool { + false + } +} + +/// Trait pour les modèles UPnP qui peuvent créer des instances. +/// +/// Un modèle représente la définition ou template d'un objet UPnP, tandis qu'une +/// instance est une occurrence concrète de cet objet. +/// +/// # Type associé +/// +/// - [`Instance`](Self::Instance) : Le type d'instance créée par ce modèle +/// +/// # Méthodes +/// +/// - [`create_instance`](Self::create_instance) : Crée une nouvelle instance +/// +/// # Hiérarchie +/// +/// ```text +/// UpnpObject +/// └─> UpnpModel +/// ``` +/// +/// # Relation avec UpnpInstance +/// +/// `UpnpModel` et [`UpnpInstance`] sont liés via leurs types associés : +/// - Le modèle spécifie quel type d'instance il crée +/// - L'instance spécifie de quel type de modèle elle provient +/// +/// # Examples +/// +/// ```ignore +/// struct DeviceModel { /* ... */ } +/// struct DeviceInstance { /* ... */ } +/// +/// impl UpnpModel for DeviceModel { +/// type Instance = DeviceInstance; +/// } +/// +/// impl UpnpInstance for DeviceInstance { +/// type Model = DeviceModel; +/// +/// fn new(model: &DeviceModel) -> Self { +/// // Création de l'instance depuis le modèle +/// } +/// } +/// +/// // Utilisation +/// let model = DeviceModel::new(); +/// let instance = model.create_instance(); // Arc +/// ``` +pub trait UpnpModel: UpnpObject { + /// Le type d'instance créée par ce modèle. + type Instance: UpnpInstance; + + /// Crée une nouvelle instance à partir de ce modèle. + /// + /// # Returns + /// + /// Un `Arc` contenant la nouvelle instance créée. + /// + /// # Implémentation par défaut + /// + /// Par défaut, appelle [`UpnpInstance::new`] avec une référence vers ce modèle + /// et encapsule le résultat dans un `Arc`. + fn create_instance(&self) -> Arc { + Arc::new(Self::Instance::new(self)) + } +} + +/// Trait pour les instances UPnP concrètes. +/// +/// Une instance représente une occurrence concrète d'un objet UPnP, créée à partir +/// d'un modèle ([`UpnpModel`]). +/// +/// # Type associé +/// +/// - [`Model`](Self::Model) : Le type du modèle dont cette instance dérive +/// +/// # Méthodes requises +/// +/// - [`new`](Self::new) : Constructeur créant l'instance depuis un modèle +/// +/// # Hiérarchie +/// +/// ```text +/// UpnpObject +/// └─> UpnpInstance +/// ``` +/// +/// # Relation avec UpnpModel +/// +/// Voir la documentation de [`UpnpModel`] pour comprendre la relation entre +/// modèles et instances. +pub trait UpnpInstance: UpnpObject { + /// Le type du modèle dont cette instance est dérivée. + type Model: UpnpModel; + + /// Crée une nouvelle instance à partir d'un modèle. + /// + /// # Arguments + /// + /// * `model` - Référence vers le modèle à partir duquel créer l'instance + /// + /// # Returns + /// + /// Une nouvelle instance initialisée depuis le modèle. + fn new(model: &Self::Model) -> Self; + + /// Indique que cet objet est une instance. + /// + /// # Returns + /// + /// Toujours `true` pour les instances. + fn is_instance(&self) -> bool { + true + } +} + +/// Trait pour les objets UPnP typés. +/// +/// Ajoute les informations de type et de nom aux objets UPnP. +/// +/// # Méthodes requises +/// +/// - [`as_upnp_object_type`](Self::as_upnp_object_type) : Accès au type de l'objet +/// +/// # Méthodes fournies +/// +/// - [`get_name`](Self::get_name) : Récupère le nom de l'objet +/// - [`get_object_type`](Self::get_object_type) : Récupère le type de l'objet +/// +/// # Hiérarchie +/// +/// ```text +/// UpnpObject +/// └─> UpnpTyped +/// ``` +pub trait UpnpTyped: UpnpObject { + /// Retourne une référence vers le type de l'objet. + fn as_upnp_object_type(&self) -> &UpnpObjectType; + + /// Retourne le nom de l'objet. + /// + /// # Returns + /// + /// Une référence vers le nom de l'objet. + fn get_name(&self) -> &String { + &self.as_upnp_object_type().name + } + + /// Retourne le type de l'objet sous forme de chaîne. + /// + /// # Returns + /// + /// Une référence vers le type de l'objet (ex: "Device", "Service", etc.). + fn get_object_type(&self) -> &String { + &self.as_upnp_object_type().object_type + } +} + +/// Trait marqueur pour les objets UPnP typés. +/// +/// Combine [`UpnpObject`] et [`UpnpTyped`] pour créer un objet avec toutes +/// les fonctionnalités de base plus les informations de type. +/// +/// # Hiérarchie +/// +/// ```text +/// UpnpObject + UpnpTyped +/// └─> UpnpTypedObject +/// ``` +/// +/// # Note +/// +/// C'est un *marker trait* (trait marqueur) sans méthodes supplémentaires. +pub trait UpnpTypedObject: UpnpObject + UpnpTyped {} + +/// Trait marqueur pour les instances typées UPnP. +/// +/// Combine [`UpnpTypedObject`] et [`UpnpInstance`] pour représenter une instance +/// concrète d'un objet typé avec toutes les fonctionnalités : +/// - Sérialisation XML/Markdown (de [`UpnpObject`]) +/// - Informations de type et nom (de [`UpnpTyped`]) +/// - Relation avec un modèle (de [`UpnpInstance`]) +/// +/// # Hiérarchie +/// +/// ```text +/// UpnpTypedObject + UpnpInstance +/// └─> UpnpTypedInstance +/// ``` +/// +/// # Note +/// +/// Ce trait ajoute la méthode [`get_model`](Self::get_model) pour accéder +/// au modèle de l'instance. Les collections d'instances ([`UpnInstanceSet`]) +/// n'ont pas cette méthode car elles contiennent plusieurs instances. +pub trait UpnpTypedInstance: UpnpTypedObject + UpnpInstance +where + Self::Model: UpnpModel +{ + /// Retourne une référence vers le modèle dont cette instance est dérivée. + /// + /// Permet d'accéder aux métadonnées et contraintes définies dans le modèle, + /// telles que les plages de valeurs autorisées, les types, les descriptions, etc. + /// + /// # Returns + /// + /// Une référence immuable vers le modèle. + /// + /// # Examples + /// + /// ```ignore + /// let instance = model.create_instance(); + /// + /// // Accéder aux propriétés du modèle depuis l'instance + /// let model_ref = instance.get_model(); + /// println!("Instance du modèle: {}", model_ref.get_name()); + /// + /// // Vérifier les contraintes définies dans le modèle + /// if let Some(range) = model_ref.get_range() { + /// println!("Plage autorisée: {:?}", range); + /// } + /// ``` + /// + /// # Use cases + /// + /// Cette méthode est particulièrement utile pour : + /// - Valider des valeurs contre les contraintes du modèle + /// - Accéder aux métadonnées sans dupliquer les informations + /// - Afficher des informations de type ou de description + /// - Implémenter des logiques conditionnelles basées sur le modèle + /// + /// # Différence avec les traits spécifiques + /// + /// Pour les variables d'état, le trait [`UpnpVariable`](crate::state_variables::UpnpVariable) + /// fournit également `get_definition()` qui est sémantiquement équivalent + /// mais spécifique au domaine des variables. + fn get_model(&self) -> &Self::Model; +} + + +/// Trait marqueur pour les collections UPnP. +/// +/// Représente un ensemble (set) d'objets UPnP. +/// +/// # Super-traits requis +/// +/// - [`UpnpObject`] : Fonctionnalités de base (XML, etc.) +/// - [`UpnpDeepClone`] : Permet le clonage profond des collections +/// +/// # Implémentation +/// +/// Ce trait surcharge [`UpnpObject::is_set`] pour retourner `true`. +/// +/// # Hiérarchie +/// +/// ```text +/// UpnpObject + UpnpDeepClone +/// └─> UpnpSet +/// ``` +/// +/// # Note sur le clonage +/// +/// Les collections UPnP contiennent généralement des `Arc` vers leurs éléments. +/// Le trait [`Clone`] (via `UpnpObject`) effectue un clonage shallow des `Arc`, +/// tandis que [`UpnpDeepClone`] clone profondément les éléments contenus. +/// +/// # Examples +/// +/// ```ignore +/// struct ServiceSet { +/// services: HashMap>, +/// } +/// +/// impl Clone for ServiceSet { +/// fn clone(&self) -> Self { +/// // Clone shallow : partage les Services via Arc +/// Self { +/// services: self.services.clone() +/// } +/// } +/// } +/// +/// impl UpnpDeepClone for ServiceSet { +/// fn deep_clone(&self) -> Self { +/// // Clone profond : crée de nouveaux Services +/// let deep_services = self.services +/// .iter() +/// .map(|(k, v)| (k.clone(), Arc::new((**v).clone()))) +/// .collect(); +/// +/// Self { +/// services: deep_services +/// } +/// } +/// } +/// ``` +pub trait UpnpSet: UpnpObject + UpnpDeepClone { + /// Indique que cet objet est une collection. + /// + /// # Returns + /// + /// Toujours `true` pour les collections. + fn is_set(&self) -> bool { + true + } +} + +/// Trait marqueur pour les collections de modèles UPnP. +/// +/// Combine [`UpnpSet`] et [`UpnpModel`] pour représenter une collection +/// de modèles qui peut elle-même créer une collection d'instances. +/// +/// # Cas d'usage +/// +/// Ce trait est utilisé quand une collection de modèles doit pouvoir instancier +/// une collection d'instances correspondante. Par exemple : +/// - Un ensemble de modèles de services d'un device qui crée un ensemble d'instances de services +/// - Une liste de modèles d'actions qui instancie une liste d'actions actives +/// - Une collection de modèles de variables d'état qui génère une collection d'instances +/// +/// # Hiérarchie +/// +/// ```text +/// UpnpSet + UpnpModel +/// └─> UpnpModelSet +/// ``` +/// +/// # Relation avec d'autres traits +/// +/// - [`UpnpSet`] : Fournit les fonctionnalités de collection +/// - [`UpnpModel`] : Fournit la capacité de créer des instances +/// - [`UpnInstanceSet`] : Représente les collections d'instances (contrepartie) +/// +/// # Note +/// +/// C'est un *marker trait* (trait marqueur) sans méthodes supplémentaires. +/// Il est automatiquement implémenté pour tous les types éligibles via une +/// blanket implementation. +/// +/// # Examples +/// +/// ```ignore +/// /// Collection de modèles de services +/// struct ServiceSetModel { +/// services: Vec>, +/// } +/// +/// /// Collection d'instances de services +/// struct ServiceSetInstance { +/// model: Arc, +/// service_instances: Vec>, +/// } +/// +/// impl UpnpObject for ServiceSetModel { /* ... */ } +/// impl UpnpSet for ServiceSetModel {} +/// +/// impl UpnpModel for ServiceSetModel { +/// type Instance = ServiceSetInstance; +/// +/// fn create_instance(&self) -> Arc { +/// // Créer des instances pour chaque service +/// let instances = self.services +/// .iter() +/// .map(|model| model.create_instance()) +/// .collect(); +/// +/// Arc::new(ServiceSetInstance { +/// model: Arc::new(self.clone()), +/// service_instances: instances, +/// }) +/// } +/// } +/// +/// // UpnpModelSet est automatiquement implémenté ! +/// +/// // Utilisation +/// let model_set = ServiceSetModel::new(); +/// let instance_set = model_set.create_instance(); // Crée toutes les instances +/// ``` +pub trait UpnpModelSet: UpnpSet + UpnpModel {} + + +/// Trait marqueur pour les collections d'instances UPnP. +/// +/// Combine [`UpnpSet`] et [`UpnpInstance`] pour représenter une collection +/// d'instances UPnP. Cela permet d'avoir des collections qui sont elles-mêmes +/// des instances créées depuis un modèle. +/// +/// # Hiérarchie +/// +/// ```text +/// UpnpSet + UpnpInstance +/// └─> UpnInstanceSet +/// ``` +/// +/// # Note +/// +/// C'est un *marker trait* (trait marqueur) sans méthodes supplémentaires. +pub trait UpnInstanceSet: UpnpSet + UpnpInstance {} + + +/// Implémentation automatique de [`UpnInstanceSet`] pour tous les types éligibles. +/// +/// Cette *blanket implementation* fournit automatiquement le trait [`UpnInstanceSet`] +/// à tout type `T` qui implémente à la fois [`UpnpSet`] et [`UpnpInstance`]. +/// +/// # Contraintes +/// +/// - `T` doit implémenter [`UpnpSet`] (collection d'objets UPnP) +/// - `T` doit implémenter [`UpnpInstance`] (instance créée depuis un modèle) +/// +/// # Pourquoi cette implémentation existe +/// +/// Certaines collections UPnP sont elles-mêmes des instances (par exemple, une +/// collection de services pour un device spécifique). Ce trait marker permet +/// d'identifier ces collections qui combinent les deux aspects. La blanket +/// implementation évite d'avoir à l'implémenter manuellement pour chaque type. +/// +/// # Utilisation +/// +/// ```ignore +/// struct ServiceSetInstance { +/// model: Arc, +/// services: Vec>, +/// } +/// +/// impl UpnpObject for ServiceSetInstance { /* ... */ } +/// impl UpnpSet for ServiceSetInstance {} +/// impl UpnpInstance for ServiceSetInstance { +/// type Model = ServiceSetModel; +/// fn new(model: &ServiceSetModel) -> Self { /* ... */ } +/// } +/// +/// // UpnInstanceSet est automatiquement implémenté ! +/// +/// fn process_instance_set(set: &T) { +/// if set.is_set() && set.is_instance() { +/// println!("C'est une collection ET une instance"); +/// } +/// } +/// ``` +impl UpnInstanceSet for T +where + T: UpnpSet + UpnpInstance +{} + +/// Implémentation automatique de [`UpnpTypedObject`] pour tous les types éligibles. +/// +/// Cette *blanket implementation* fournit automatiquement le trait [`UpnpTypedObject`] +/// à tout type `T` qui implémente à la fois [`UpnpObject`] et [`UpnpTyped`]. +/// +/// # Contraintes +/// +/// - `T` doit implémenter [`UpnpObject`] (fonctionnalités de base UPnP) +/// - `T` doit implémenter [`UpnpTyped`] (informations de type et nom) +/// +/// # Utilisation +/// +/// ```ignore +/// struct Device { +/// object_type: UpnpObjectType, +/// } +/// +/// impl UpnpObject for Device { /* ... */ } +/// impl UpnpTyped for Device { /* ... */ } +/// +/// // UpnpTypedObject est automatiquement implémenté ! +/// fn process(obj: &T) { +/// println!("{}", obj.get_name()); +/// } +/// ``` +impl UpnpTypedObject for T +where + T: UpnpObject + UpnpTyped +{} + + +/// Implémentation automatique de [`UpnpModelSet`] pour tous les types éligibles. +/// +/// Cette *blanket implementation* fournit automatiquement le trait [`UpnpModelSet`] +/// à tout type `T` qui implémente à la fois [`UpnpSet`] et [`UpnpModel`]. +/// +/// # Contraintes +/// +/// - `T` doit implémenter [`UpnpSet`] (collection d'objets UPnP) +/// - `T` doit implémenter [`UpnpModel`] (peut créer des instances) +/// +/// # Pourquoi cette implémentation existe +/// +/// [`UpnpModelSet`] est un *marker trait* qui identifie les collections pouvant +/// créer des collections d'instances. Plutôt que de demander aux développeurs +/// d'écrire manuellement `impl UpnpModelSet for MyType {}`, cette blanket +/// implementation le fait automatiquement dès que les traits requis sont implémentés. +/// +/// # Fonctionnement +/// +/// Lorsque vous définissez une collection de modèles : +/// +/// ```ignore +/// struct ActionSetModel { +/// actions: Vec>, +/// } +/// +/// impl UpnpObject for ActionSetModel { /* ... */ } +/// impl UpnpSet for ActionSetModel {} +/// +/// impl UpnpModel for ActionSetModel { +/// type Instance = ActionSetInstance; +/// fn create_instance(&self) -> Arc { /* ... */ } +/// } +/// ``` +/// +/// Le compilateur Rust vérifie automatiquement que `ActionSetModel` satisfait +/// toutes les contraintes (implémente `UpnpSet` ET `UpnpModel`) et applique +/// donc `UpnpModelSet` sans code supplémentaire. +/// +/// # Utilisation dans des signatures génériques +/// +/// ```ignore +/// fn process_model_set(set: &T) { +/// println!("Processing model set that can create instances"); +/// let instance = set.create_instance(); +/// // ... +/// } +/// ``` +/// +/// # Différence avec UpnInstanceSet +/// +/// - [`UpnpModelSet`] : Collection de **modèles** (peut créer des instances) +/// - [`UpnInstanceSet`] : Collection d'**instances** (créée depuis un modèle) +impl UpnpModelSet for T +where + T: UpnpSet + UpnpModel +{} + diff --git a/pmoupnp/src/server/logs/mod.rs b/pmoupnp/src/server/logs/mod.rs new file mode 100644 index 00000000..6ff83ece --- /dev/null +++ b/pmoupnp/src/server/logs/mod.rs @@ -0,0 +1,159 @@ +// logs.rs +mod sselayer; + +pub use sselayer::SseLayer; + +use std::{ + collections::VecDeque, + sync::{Arc, RwLock}, + time::SystemTime, +}; + +use axum::{ + Json, + extract::{Query, State}, + response::{ + IntoResponse, + sse::{Event, KeepAlive, Sse}, + }, +}; +use serde::{Deserialize, Serialize}; +use tokio::sync::broadcast; + +/// Représente une entrée de log +#[derive(Debug, Clone, Serialize)] +pub struct LogEntry { + pub timestamp: SystemTime, + pub level: String, + pub target: String, + pub message: String, +} + +/// Buffer circulaire partagé +#[derive(Clone)] +pub struct LogState { + buffer: Arc>>, + tx: broadcast::Sender, +} + +impl LogState { + pub fn new(capacity: usize) -> Self { + Self { + buffer: Arc::new(RwLock::new(VecDeque::with_capacity(capacity))), + tx: broadcast::channel(1000).0, + } + } + + fn push(&self, entry: LogEntry) { + let mut buf = self.buffer.write().unwrap(); + if buf.len() == buf.capacity() { + buf.pop_front(); + } + buf.push_back(entry.clone()); + let _ = self.tx.send(entry); + } + + pub fn subscribe(&self) -> broadcast::Receiver { + self.tx.subscribe() + } + + pub fn dump(&self) -> Vec { + self.buffer.read().unwrap().iter().cloned().collect() + } +} + +/// Query params pour /log-sse +#[derive(Debug, Deserialize)] +pub struct LogQuery { + #[serde(default)] + pub error: Option, + #[serde(default)] + pub warn: Option, + #[serde(default)] + pub info: Option, + #[serde(default)] + pub debug: Option, + #[serde(default)] + pub trace: Option, + #[serde(default)] + pub search: Option, +} + +/// Handler SSE +// Dans logs.rs +pub async fn log_sse( + State(state): State, + Query(params): Query, +) -> impl IntoResponse { + let mut rx = state.subscribe(); + + // Récupérer l'historique du buffer + let history = state.dump(); + + let stream = async_stream::stream! { + // 1. Envoyer d'abord tous les logs historiques + for entry in history { + if !filter_entry(&entry, ¶ms) { + continue; + } + let json = serde_json::to_string(&entry).unwrap(); + yield Ok::<_, axum::Error>(Event::default().data(json)); + } + + // 2. Puis streamer les nouveaux logs en temps réel + while let Ok(entry) = rx.recv().await { + if !filter_entry(&entry, ¶ms) { + continue; + } + let json = serde_json::to_string(&entry).unwrap(); + yield Ok::<_, axum::Error>(Event::default().data(json)); + } + }; + + Sse::new(stream).keep_alive(KeepAlive::default()) +} + +/// Handler REST (dump JSON du buffer) +pub async fn log_dump(State(state): State) -> impl IntoResponse { + Json(state.dump()) +} + +/// Fonction de filtrage +fn filter_entry(entry: &LogEntry, q: &LogQuery) -> bool { + // Filtrage par niveau + let lvl = entry.level.to_lowercase(); + let mut allowed = false; + + if let Some(true) = q.error { + allowed |= lvl == "error"; + } + if let Some(true) = q.warn { + allowed |= lvl == "warn"; + } + if let Some(true) = q.info { + allowed |= lvl == "info"; + } + if let Some(true) = q.debug { + allowed |= lvl == "debug"; + } + if let Some(true) = q.trace { + allowed |= lvl == "trace"; + } + + // si aucun flag → tout est autorisé + if !(q.error.unwrap_or(false) + || q.warn.unwrap_or(false) + || q.info.unwrap_or(false) + || q.debug.unwrap_or(false) + || q.trace.unwrap_or(false)) + { + allowed = true; + } + + // Filtrage par mot-clé + if let Some(search) = &q.search { + allowed &= entry.message.contains(search) || entry.target.contains(search); + } + + allowed +} diff --git a/pmoupnp/src/server/logs/sselayer.rs b/pmoupnp/src/server/logs/sselayer.rs new file mode 100644 index 00000000..4d2ec3fa --- /dev/null +++ b/pmoupnp/src/server/logs/sselayer.rs @@ -0,0 +1,63 @@ +use tracing::field::{Field, Visit}; +use tracing::{Event, Subscriber}; +use tracing_subscriber::{Layer, layer::Context}; + +use super::{LogEntry, LogState}; +use std::time::SystemTime; + +struct LogVisitor { + message: String, +} + +impl LogVisitor { + fn new() -> Self { + Self { + message: String::new(), + } + } +} + +impl Visit for LogVisitor { + fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { + // capture le champ "message" ou concatène les autres + if field.name() == "message" { + self.message = format!("{:?}", value); + } else { + if !self.message.is_empty() { + self.message.push(' '); + } + self.message + .push_str(&format!("{}={:?}", field.name(), value)); + } + } +} + +/// Layer de tracing qui pousse les events dans le buffer +pub struct SseLayer { + state: LogState, +} + +impl SseLayer { + pub fn new(state: LogState) -> Self { + Self { state } + } +} + +impl Layer for SseLayer +where + S: Subscriber, +{ + fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { + let mut visitor = LogVisitor::new(); + event.record(&mut visitor); + + let entry = LogEntry { + timestamp: SystemTime::now(), + level: event.metadata().level().to_string(), + target: event.metadata().target().to_string(), + message: visitor.message, + }; + + self.state.push(entry); + } +} diff --git a/pmoupnp/src/server/mod.rs b/pmoupnp/src/server/mod.rs new file mode 100644 index 00000000..303931ce --- /dev/null +++ b/pmoupnp/src/server/mod.rs @@ -0,0 +1,566 @@ +//! # Module Server - API de haut niveau pour Axum +//! +//! Ce module fournit une abstraction simple et ergonomique pour créer des serveurs HTTP +//! avec Axum, en cachant la complexité de la configuration et du routage. +//! +//! ## Fonctionnalités +//! +//! - 🚀 **Routes JSON simples** : Ajoutez des endpoints API avec `add_route()` +//! - 📁 **Fichiers statiques** : Servez des assets avec `add_dir()` +//! - ⚛️ **Applications SPA** : Support pour Vue.js/React avec `add_spa()` +//! - 🔀 **Redirections** : Redirigez des routes avec `add_redirect()` +//! - 🎯 **Handlers personnalisés** : Support SSE, WebSocket, etc. avec `add_handler_with_state()` +//! - 📚 **Documentation API** : OpenAPI/Swagger automatique avec `add_openapi()` +//! - ⚡ **Gestion gracieuse** : Arrêt propre sur Ctrl+C + +pub mod logs; + +use axum::handler::Handler; +use axum::response::Redirect; +use axum::routing::get; +use axum::{Json, Router}; +use axum_embed::ServeEmbed; +use pmoconfig::get_config; +use rust_embed::RustEmbed; +use serde::Serialize; +use std::{net::SocketAddr, sync::Arc}; +use tokio::{signal, sync::RwLock, task::JoinHandle}; +use tracing::{info, warn, debug, error}; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; + +/// Info serveur sérialisable +#[derive(Clone, Serialize, utoipa::ToSchema)] +pub struct ServerInfo { + /// Nom du serveur + pub name: String, + /// URL de base + pub base_url: String, + /// Port HTTP + pub http_port: u16, +} + +/// Serveur principal +pub struct Server { + name: String, + base_url: String, + http_port: u16, + router: Arc>, + api_router: Arc>>, + join_handle: Option>, +} + +#[derive(RustEmbed, Clone)] +#[folder = "webapp/dist"] +pub struct Webapp; + +impl Server { + /// Crée une nouvelle instance de serveur + /// + /// # Arguments + /// + /// * `name` - Nom du serveur (pour les logs) + /// * `base_url` - URL de base (ex: "http://localhost:3000") + /// * `http_port` - Port HTTP à écouter + /// + /// # Exemple + /// + /// ```rust + /// # use pmoupnp::server::Server; + /// let server = Server::new("MyAPI", "http://localhost:3000", 3000); + /// ``` + pub fn new(name: impl Into, base_url: impl Into, http_port: u16) -> Self { + Self { + name: name.into(), + base_url: base_url.into(), + http_port, + router: Arc::new(RwLock::new(Router::new())), + api_router: Arc::new(RwLock::new(None)), + join_handle: None, + } + } + + pub fn new_configured() -> Self { + let config = get_config(); + let url = config.get_base_url(); + let port = config.get_http_port(); + + return Self::new("PMO-Music-Server", url, port); + } + + /// Ajoute une route JSON dynamique + /// + /// Crée un endpoint qui retourne du JSON. La closure fournie sera appelée + /// à chaque requête GET sur le chemin spécifié. + /// + /// # Arguments + /// + /// * `path` - Chemin de la route (ex: "/api/hello") + /// * `f` - Closure async retournant une valeur sérialisable + /// + /// # Exemple + /// + /// ```rust,no_run + /// # use pmoupnp::server::Server; + /// # #[tokio::main] + /// # async fn main() { + /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); + /// server.add_route("/api/status", || async { + /// serde_json::json!({ + /// "status": "online", + /// "version": "1.0.0" + /// }) + /// }).await; + /// # } + /// ``` + pub async fn add_route(&mut self, path: &str, f: F) + where + F: Fn() -> Fut + Send + Sync + 'static, + Fut: std::future::Future + Send + 'static, + T: Serialize + Send + 'static, + { + let f = Arc::new(f); + + let handler = { + let f = f.clone(); + move || { + let f = f.clone(); + async move { Json(f().await) } + } + }; + + let route = Router::new().route("/", get(handler)); + + let mut r = self.router.write().await; + *r = std::mem::take(&mut *r).nest(path, route); + } + + /// Ajoute un répertoire de fichiers statiques + /// + /// Sert des fichiers embarqués via `RustEmbed`. Les fichiers sont compilés + /// dans le binaire à la compilation. + /// + /// # Arguments + /// + /// * `path` - Chemin où monter les fichiers statiques + /// + /// # Type Parameter + /// + /// * `E` - Type RustEmbed définissant le répertoire à servir + /// + /// # Exemple + /// + /// ```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); + /// server.add_dir::("/assets").await; + /// // Les fichiers de static/ sont accessibles via /assets/* + /// # } + /// ``` + pub async fn add_dir(&mut self, path: &str) + where + 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 { + let route = Router::new().fallback_service(serve); + *r = std::mem::take(&mut *r).nest(path, route); + } + } + + /// Ajoute une Single Page Application (SPA) + /// + /// Sert une application JavaScript moderne (Vue.js, React, etc.) avec support + /// du routage côté client. Tous les chemins non trouvés renvoient `index.html` + /// pour permettre au routeur JavaScript de gérer la navigation. + /// + /// # Arguments + /// + /// * `path` - Chemin où monter l'application (souvent "/" ou "/app") + /// + /// # Type Parameter + /// + /// * `E` - Type RustEmbed contenant les fichiers de la SPA + /// + /// # Exemple avec Vue.js + /// + /// ```rust,no_run + /// # use pmoupnp::server::Server; + /// # use rust_embed::RustEmbed; + /// #[derive(RustEmbed, Clone)] + /// #[folder = "webapp/dist"] // Build output de Vue.js + /// struct WebApp; + /// + /// # #[tokio::main] + /// # async fn main() { + /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); + /// server.add_spa::("/").await; + /// // L'app Vue.js gère toutes les routes comme /about, /users, etc. + /// # } + /// ``` + /// + /// # Note + /// + /// Pour Vue.js/Vite, configure le `base` dans `vite.config.js` si tu montes + /// sur un sous-chemin : + /// ```javascript + /// export default { + /// base: '/app/' + /// } + /// ``` + pub async fn add_spa(&mut self, path: &str) + where + E: RustEmbed + Clone + Send + Sync + 'static, + { + let serve = ServeEmbed::::with_parameters( + Some("index.html".to_string()), + 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 { + let route = Router::new().fallback_service(serve); + *r = std::mem::take(&mut *r).nest(path, route); + } + } + + /// Ajoute un handler Axum personnalisé + /// + /// Pour des cas d'usage avancés nécessitant un contrôle complet sur le handler. + /// + /// # Arguments + /// + /// * `path` - Chemin de la route + /// * `handler` - Handler Axum + /// + /// # Exemple + /// + /// ```rust,no_run + /// # use pmoupnp::server::Server; + /// # use axum::response::Html; + /// # #[tokio::main] + /// # async fn main() { + /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); + /// async fn custom_handler() -> Html<&'static str> { + /// Html("

Custom Response

") + /// } + /// + /// server.add_handler("/custom", custom_handler).await; + /// # } + /// ``` + pub async fn add_handler(&mut self, path: &str, handler: H) + where + H: Handler, + T: 'static, + { + let route = Router::new().route("/", get(handler)); + + let mut r = self.router.write().await; + *r = std::mem::take(&mut *r).nest(path, route); + } + + /// Ajoute un handler avec state (pour SSE, extracteurs, etc.) + /// + /// Permet d'utiliser des extracteurs Axum comme `State`, `Query`, etc. + /// Idéal pour Server-Sent Events (SSE), WebSockets ou tout handler nécessitant un état partagé. + /// + /// # Arguments + /// + /// * `path` - Chemin de la route + /// * `handler` - Handler Axum avec extracteurs + /// * `state` - État partagé (doit être Clone + Send + Sync) + /// + /// # Exemple avec SSE + /// + /// ```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! { + /// while let Ok(msg) = rx.recv().await { + /// yield Ok(Event::default().data(msg)); + /// } + /// }; + /// Sse::new(stream).keep_alive(KeepAlive::default()) + /// } + /// + /// 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 + H: Handler, + T: 'static, + S: Clone + Send + Sync + 'static, + { + 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); + } + + /// Ajoute un handler POST avec state + /// + /// Similaire à `add_handler_with_state` mais pour les requêtes POST. + /// + /// # Arguments + /// + /// * `path` - Chemin de la route + /// * `handler` - Handler Axum pour POST + /// * `state` - État partagé + pub async fn add_post_handler_with_state(&mut self, path: &str, handler: H, state: S) + where + H: Handler, + T: 'static, + S: Clone + Send + Sync + 'static, + { + let route = Router::new() + .route("/", axum::routing::post(handler)) + .with_state(state); + + let mut r = self.router.write().await; + *r = std::mem::take(&mut *r).nest(path, route); + } + + /// Ajoute une redirection HTTP + /// + /// Redirige automatiquement les requêtes d'un chemin vers un autre avec un code 308 (permanent). + /// + /// # Arguments + /// + /// * `from` - Chemin source (peut être "/" pour la racine) + /// * `to` - Chemin de destination + /// + /// # Exemple + /// + /// ```rust,no_run + /// # use pmoupnp::server::Server; + /// # #[tokio::main] + /// # async fn main() { + /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); + /// // Rediriger la racine vers /app + /// server.add_redirect("/", "/app").await; + /// # } + /// ``` + pub async fn add_redirect(&mut self, from: &str, to: &str) { + let to = to.to_string(); + let handler = move || { + let to = to.clone(); + async move { Redirect::permanent(&to) } + }; + + let mut r = self.router.write().await; + + if from == "/" { + // Pour la racine, utiliser merge au lieu de nest + let route = Router::new().route("/", get(handler)); + *r = std::mem::take(&mut *r).merge(route); + } else { + let route = Router::new().route("/", get(handler)); + *r = std::mem::take(&mut *r).nest(from, route); + } + } + + /// Ajoute une API documentée avec OpenAPI + /// + /// Monte un routeur d'API sous `/api` et active Swagger UI sur `/swagger-ui` + /// + /// # Arguments + /// + /// * `api_router` - Router Axum contenant les routes API + /// * `openapi` - Spécification OpenAPI générée par utoipa + /// + /// # Exemple + /// + /// ```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( + /// paths(get_users), + /// components(schemas(User)) + /// )] + /// struct ApiDoc; + /// + /// #[utoipa::path( + /// get, + /// path = "/users", + /// responses((status = 200, description = "List users")) + /// )] + /// async fn get_users() -> Json> { + /// Json(vec![]) + /// } + /// + /// let api_router = Router::new() + /// .route("/users", get(get_users)); + /// + /// server.add_openapi(api_router, ApiDoc::openapi()).await; + /// ``` + pub async fn add_openapi(&mut self, api_router: Router, openapi: utoipa::openapi::OpenApi) { + // Stocker le routeur API + let mut api_r = self.api_router.write().await; + *api_r = Some(api_router); + + // Ajouter Swagger UI + 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); + } + + /// Démarre le serveur HTTP + /// + /// Lance le serveur sur le port configuré et met en place la gestion + /// de Ctrl+C pour un arrêt gracieux. + /// + /// # Exemple + /// + /// ```rust,no_run + /// # use pmoupnp::server::Server; + /// # #[tokio::main] + /// # async fn main() { + /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); + /// server.start().await; + /// server.wait().await; // Attend Ctrl+C + /// # } + /// ``` + 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); + + // Merger le routeur API si présent + let api_router = self.api_router.read().await; + if let Some(api_r) = api_router.as_ref() { + let mut r = self.router.write().await; + *r = std::mem::take(&mut *r).nest("/api", api_r.clone()); + } + drop(api_router); + + let router = self.router.clone(); + + let server_task = tokio::spawn(async move { + let r = router.read().await.clone(); + let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); + axum::serve(listener, r.into_make_service()).await.unwrap(); + }); + + let shutdown_task = tokio::spawn(async move { + signal::ctrl_c().await.expect("failed to listen for ctrl_c"); + info!("Ctrl+C reçu, arrêt gracieux"); + }); + + self.join_handle = Some(tokio::spawn(async move { + tokio::select! { + _ = server_task => {}, + _ = shutdown_task => {}, + } + })); + } + + /// Attend la fin du serveur + pub async fn wait(&mut self) { + if let Some(h) = self.join_handle.take() { + let _ = h.await; + } + } + + /// Récupère les infos du serveur + pub fn info(&self) -> ServerInfo { + ServerInfo { + name: self.name.clone(), + base_url: self.base_url.clone(), + http_port: self.http_port, + } + } +} + +/// Builder pattern +pub struct ServerBuilder { + name: String, + base_url: String, + http_port: u16, +} + +impl ServerBuilder { + /// Crée un nouveau builder + /// + /// # Arguments + /// + /// * `name` - Nom du serveur + /// * `base_url` - URL de base (ex: "http://localhost:3000") + /// * `http_port` - Port HTTP + pub fn new(name: impl Into, base_url: impl Into, http_port: u16) -> Self { + Self { + name: name.into(), + base_url: base_url.into(), + http_port, + } + } + + pub fn new_configured() -> Self { + let config = get_config(); + Self { + name: "PMO-Music-Server".to_string(), + base_url: config.get_base_url(), + http_port: config.get_http_port() + } + } + + /// Construit le serveur + /// + /// Consomme le builder et retourne une instance de `Server` prête à l'emploi. + /// + /// # Exemple + /// + /// ```rust + /// # use pmoupnp::server::ServerBuilder; + /// let mut server = ServerBuilder::new("MyAPI", "http://localhost:3000", 3000) + /// .build(); + /// ``` + 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/services/errors.rs b/pmoupnp/src/services/errors.rs new file mode 100644 index 00000000..5b7ab2d9 --- /dev/null +++ b/pmoupnp/src/services/errors.rs @@ -0,0 +1,62 @@ +//! Erreurs du module services. + +use thiserror::Error; + +/// Erreurs liées aux services UPnP. +/// +/// Cette énumération couvre toutes les erreurs possibles lors de la manipulation +/// de services UPnP, incluant les erreurs de validation, de configuration et d'exécution. +#[derive(Error, Debug)] +pub enum ServiceError { + /// Erreur générale du service. + #[error("Service error: {0}")] + GeneralError(String), + + /// Erreur de validation (paramètres invalides). + #[error("Validation error: {0}")] + ValidationError(String), + + /// Erreur lors d'une opération sur un ensemble (Set). + #[error("Set operation error: {0}")] + SetError(String), + + /// Erreur liée à une action. + #[error("Action error: {0}")] + ActionError(String), + + /// Erreur liée à une variable d'état. + #[error("State variable error: {0}")] + StateVariableError(String), + + /// Erreur de configuration. + #[error("Configuration error: {0}")] + ConfigError(String), + + /// Erreur réseau ou HTTP. + #[error("Network error: {0}")] + NetworkError(String), + + /// Erreur de sérialisation XML. + #[error("XML serialization error: {0}")] + XmlError(String), + + /// Erreur lors du traitement SOAP. + #[error("SOAP error: {0}")] + SoapError(String), +} + +impl From for ServiceError { + fn from(err: std::io::Error) -> Self { + ServiceError::GeneralError(format!("IO error: {}", err)) + } +} + +impl From for ServiceError { + fn from(err: crate::UpnpObjectSetError) -> Self { + match err { + crate::UpnpObjectSetError::AlreadyExists(name) => { + ServiceError::SetError(format!("Object already exists: {}", name)) + } + } + } +} \ No newline at end of file diff --git a/pmoupnp/src/services/macros.rs b/pmoupnp/src/services/macros.rs new file mode 100644 index 00000000..ab1f2fa4 --- /dev/null +++ b/pmoupnp/src/services/macros.rs @@ -0,0 +1,109 @@ +/// Macro pour définir facilement un service UPnP avec ses variables et actions. +/// +/// Cette macro simplifie la création de services UPnP statiques en générant +/// automatiquement le code nécessaire pour initialiser un service avec ses +/// variables d'état et ses actions. +/// +/// # Syntaxe +/// +/// ```ignore +/// define_service! { +/// pub static SERVICE_NAME = "ServiceName" { +/// variables: [ +/// VARIABLE1, +/// VARIABLE2, +/// ], +/// actions: [ +/// ACTION1, +/// ACTION2, +/// ] +/// } +/// } +/// ``` +/// +/// # Arguments +/// +/// - `SERVICE_NAME` : Nom de la constante statique Rust +/// - `"ServiceName"` : Nom du service UPnP (chaîne littérale) +/// - `variables:` : Section listant les références aux variables d'état +/// - `actions:` : Section listant les références aux actions +/// +/// # Type de retour +/// +/// La macro génère une `Lazy>` qui sera initialisée lors du premier accès. +/// +/// # Prérequis +/// +/// Les variables et actions référencées doivent être définies comme `Lazy>`. +/// +/// # Examples +/// +/// ```ignore +/// use once_cell::sync::Lazy; +/// use std::sync::Arc; +/// +/// // Définir les variables et actions ailleurs +/// pub static TRANSPORT_STATE: Lazy> = ...; +/// pub static PLAY: Lazy> = ...; +/// pub static STOP: Lazy> = ...; +/// +/// // Définir le service +/// define_service! { +/// pub static AVTRANSPORT = "AVTransport" { +/// variables: [ +/// TRANSPORT_STATE, +/// TRANSPORT_URI, +/// ], +/// actions: [ +/// PLAY, +/// STOP, +/// PAUSE, +/// ] +/// } +/// } +/// +/// // Utilisation +/// fn main() { +/// let service = &*AVTRANSPORT; +/// println!("Service: {}", service.name()); +/// } +/// ``` +/// +/// # Notes d'implémentation +/// +/// - Les `Arc` et `Arc` sont clonés +/// - Le service est wrappé dans un `Arc` +/// - Initialisation paresseuse via `Lazy` (thread-safe) +/// - Utilise `.expect()` pour les erreurs d'ajout +#[macro_export] +macro_rules! define_service { + (pub static $name:ident = $service_name:literal { + variables: [ + $($var:expr),* $(,)? + ], + actions: [ + $($action:expr),* $(,)? + ] + }) => { + pub static $name: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| { + use $crate::UpnpTyped; + + let mut svc = $crate::services::Service::new($service_name.to_string()); + + $( + svc.add_variable(std::sync::Arc::clone(&*$var)) + .expect(&format!("Cannot add variable {} to service {}", + (*$var).get_name(), svc.name())); + )* + + $( + svc.add_action(std::sync::Arc::clone(&*$action)) + .expect(&format!("Cannot add action {} to service {}", + (*$action).get_name(), svc.name())); + )* + + std::sync::Arc::new(svc) + }); + }; +} diff --git a/pmoupnp/src/services/mod.rs b/pmoupnp/src/services/mod.rs new file mode 100644 index 00000000..dda54f64 --- /dev/null +++ b/pmoupnp/src/services/mod.rs @@ -0,0 +1,515 @@ +//! # Module Services - Gestion des services UPnP +//! +//! Ce module implémente les services UPnP selon la spécification UPnP Device Architecture. +//! Un service UPnP contient des actions (méthodes appelables) et des variables d'état +//! (propriétés observables). +//! +//! ## Architecture +//! +//! - [`Service`] : Modèle définissant la structure d'un service +//! - [`ServiceInstance`] : Instance concrète d'un service attachée à un device +//! +//! ## Fonctionnalités +//! +//! - ✅ Actions UPnP avec arguments typés +//! - ✅ Variables d'état avec notifications d'événements +//! - ✅ Génération SCPD (Service Control Protocol Description) +//! - ✅ Endpoints SOAP pour le contrôle +//! - ✅ Gestion des abonnements aux événements (SUBSCRIBE/UNSUBSCRIBE) +//! - ✅ Notifications automatiques des changements d'état +//! +//! ## Examples +//! +//! ```rust +//! use pmoupnp::services::Service; +//! use pmoupnp::state_variables::StateVariable; +//! use pmoupnp::variable_types::StateVarType; +//! use std::sync::Arc; +//! +//! // Créer un service +//! let mut service = Service::new("AVTransport".to_string()); +//! service.set_version(1).unwrap(); +//! +//! // Ajouter une variable d'état +//! let transport_state = Arc::new( +//! StateVariable::new(StateVarType::String, "TransportState".to_string()) +//! ); +//! service.add_variable(transport_state); +//! +//! // Créer une instance +//! let instance = service.create_instance(); +//! ``` + +mod errors; +mod macros; +mod service_instance; +mod service_methods; + +use std::sync::Arc; + +pub use errors::ServiceError; +pub use service_instance::ServiceInstance; +use xmltree::{Element, EmitterConfig, XMLNode}; + +use crate::{actions::ActionSet, state_variables::StateVariableSet, UpnpObject, UpnpObjectType}; + +/// Service UPnP (modèle). +/// +/// Représente la définition d'un service UPnP avec ses actions et variables d'état. +/// Un service est attaché à un device et expose des fonctionnalités via SOAP. +/// +/// # Structure +/// +/// Un service UPnP contient : +/// - Un identifiant unique (`identifier`) +/// - Une version (ex: 1, 2, 3...) +/// - Un ensemble d'actions ([`ActionSet`]) +/// - Une table de variables d'état ([`StateVariableSet`]) +/// +/// # Cycle de vie +/// +/// 1. Création avec [`Service::new`] +/// 2. Configuration (ajout d'actions et variables) +/// 3. Instanciation avec [`create_instance`](crate::UpnpModel::create_instance) +/// +/// # Examples +/// +/// ```rust +/// # use pmoupnp::services::Service; +/// # use pmoupnp::state_variables::StateVariable; +/// # use pmoupnp::variable_types::StateVarType; +/// # use std::sync::Arc; +/// let mut service = Service::new("ContentDirectory".to_string()); +/// service.set_identifier("urn:upnp-org:serviceId:ContentDirectory".to_string()); +/// service.set_version(1).unwrap(); +/// +/// // Ajouter une variable d'état +/// let search_caps = Arc::new( +/// StateVariable::new(StateVarType::String, "SearchCapabilities".to_string()) +/// ); +/// service.add_variable(search_caps); +/// ``` +#[derive(Debug, Clone)] +pub struct Service { + /// Métadonnées de l'objet UPnP + object: UpnpObjectType, + + /// Identifiant du service (ex: "urn:upnp-org:serviceId:AVTransport") + identifier: String, + + /// Version du service (>= 1) + version: u32, + + /// Actions disponibles dans ce service + actions: ActionSet, + + /// Variables d'état du service + state_table: StateVariableSet, +} + +impl Service { + /// Crée un nouveau service UPnP. + /// + /// # Arguments + /// + /// * `name` - Nom du service (ex: "AVTransport", "RenderingControl") + /// + /// # Returns + /// + /// Un nouveau service avec : + /// - Identifiant initialisé au nom + /// - Version 1 par défaut + /// - Collections vides d'actions et de variables + /// + /// # Examples + /// + /// ```rust + /// # use pmoupnp::services::Service; + /// let service = Service::new("AVTransport".to_string()); + /// assert_eq!(service.name(), "AVTransport"); + /// assert_eq!(service.version(), 1); + /// ``` + pub fn new(name: String) -> Self { + Self { + object: UpnpObjectType { + name: name.clone(), + object_type: "Service".to_string(), + }, + identifier: name, + version: 1, + state_table: StateVariableSet::new(), + actions: ActionSet::new(), + } + } + + /// Retourne le nom du service. + /// + /// # Examples + /// + /// ```rust + /// # use pmoupnp::services::Service; + /// let service = Service::new("AVTransport".to_string()); + /// assert_eq!(service.name(), "AVTransport"); + /// ``` + pub fn name(&self) -> &str { + &self.object.name + } + + /// Retourne le type d'objet. + /// + /// # Examples + /// + /// ```rust + /// # use pmoupnp::services::Service; + /// let service = Service::new("AVTransport".to_string()); + /// assert_eq!(service.type_id(), "Service"); + /// ``` + pub fn type_id(&self) -> &str { + &self.object.object_type + } + + /// Retourne l'identifiant du service. + /// + /// # Examples + /// + /// ```rust + /// # use pmoupnp::services::Service; + /// let mut service = Service::new("AVTransport".to_string()); + /// service.set_identifier("urn:upnp-org:serviceId:AVTransport".to_string()); + /// assert_eq!(service.identifier(), "urn:upnp-org:serviceId:AVTransport"); + /// ``` + pub fn identifier(&self) -> &str { + &self.identifier + } + + /// Définit l'identifiant du service. + /// + /// # Arguments + /// + /// * `id` - Nouvel identifiant (typiquement un URN UPnP) + /// + /// # Examples + /// + /// ```rust + /// # use pmoupnp::services::Service; + /// let mut service = Service::new("AVTransport".to_string()); + /// service.set_identifier("urn:upnp-org:serviceId:AVTransport".to_string()); + /// ``` + pub fn set_identifier(&mut self, id: String) { + self.identifier = id; + } + + /// Retourne la version du service. + /// + /// # Examples + /// + /// ```rust + /// # use pmoupnp::services::Service; + /// let service = Service::new("AVTransport".to_string()); + /// assert_eq!(service.version(), 1); + /// ``` + pub fn version(&self) -> u32 { + self.version + } + + /// Définit la version du service. + /// + /// # Arguments + /// + /// * `version` - Numéro de version (doit être >= 1) + /// + /// # Errors + /// + /// Retourne une erreur si la version est < 1. + /// + /// # Examples + /// + /// ```rust + /// # use pmoupnp::services::Service; + /// let mut service = Service::new("AVTransport".to_string()); + /// assert!(service.set_version(2).is_ok()); + /// assert!(service.set_version(0).is_err()); + /// ``` + pub fn set_version(&mut self, version: u32) -> Result<(), ServiceError> { + if version < 1 { + return Err(ServiceError::ValidationError( + "Version must be >= 1".to_string(), + )); + } + self.version = version; + Ok(()) + } + + /// Ajoute une variable d'état au service. + /// + /// # Arguments + /// + /// * `sv` - Variable d'état à ajouter + /// + /// # Errors + /// + /// Retourne une erreur si une variable avec le même nom existe déjà. + /// + /// # Examples + /// + /// ```rust + /// # use pmoupnp::services::Service; + /// # use pmoupnp::state_variables::StateVariable; + /// # use pmoupnp::variable_types::StateVarType; + /// # use std::sync::Arc; + /// let mut service = Service::new("AVTransport".to_string()); + /// let var = Arc::new( + /// StateVariable::new(StateVarType::String, "TransportState".to_string()) + /// ); + /// service.add_variable(var).unwrap(); + /// ``` + pub fn add_variable( + &mut self, + sv: Arc, + ) -> Result<(), ServiceError> { + self.state_table + .insert(sv) + .map_err(|e| ServiceError::SetError(format!("Failed to add variable: {:?}", e))) + } + + /// Vérifie si une variable d'état existe dans le service. + /// + /// # Arguments + /// + /// * `sv` - Variable à rechercher + /// + /// # Returns + /// + /// `true` si la variable existe, `false` sinon. + /// + /// # Examples + /// + /// ```rust + /// # use pmoupnp::services::Service; + /// # use pmoupnp::state_variables::StateVariable; + /// # use pmoupnp::variable_types::StateVarType; + /// # use std::sync::Arc; + /// let mut service = Service::new("AVTransport".to_string()); + /// let var = Arc::new( + /// StateVariable::new(StateVarType::String, "TransportState".to_string()) + /// ); + /// service.add_variable(var.clone()).unwrap(); + /// assert!(service.contains_variable(var)); + /// ``` + pub fn contains_variable(&self, sv: Arc) -> bool { + self.state_table.contains(sv) + } + + /// Retourne toutes les variables d'état du service. + /// + /// # Examples + /// + /// ```rust + /// # use pmoupnp::services::Service; + /// let service = Service::new("AVTransport".to_string()); + /// for var in service.variables() { + /// println!("Variable: {}", var.get_name()); + /// } + /// ``` + pub fn variables(&self) -> Vec> { + self.state_table.all() + } + + /// Ajoute une action au service. + /// + /// # Arguments + /// + /// * `action` - Action à ajouter + /// + /// # Errors + /// + /// Retourne une erreur si une action avec le même nom existe déjà. + /// + /// # Examples + /// + /// ```rust + /// # use pmoupnp::services::Service; + /// # use pmoupnp::actions::Action; + /// # use std::sync::Arc; + /// let mut service = Service::new("AVTransport".to_string()); + /// let action = Arc::new(Action::new("Play".to_string())); + /// service.add_action(action).unwrap(); + /// ``` + pub fn add_action(&mut self, action: Arc) -> Result<(), ServiceError> { + self.actions + .insert(action) + .map_err(|e| ServiceError::SetError(format!("Failed to add action: {:?}", e))) + } + + /// Retourne toutes les actions du service. + /// + /// # Examples + /// + /// ```rust + /// # use pmoupnp::services::Service; + /// let service = Service::new("AVTransport".to_string()); + /// for action in service.actions() { + /// println!("Action: {}", action.get_name()); + /// } + /// ``` + pub fn actions(&self) -> Vec> { + self.actions.all() + } + + /// Retourne le type de service UPnP. + /// + /// Format: `urn:schemas-upnp-org:service:{name}:{version}` + /// + /// # Examples + /// + /// ```rust + /// # use pmoupnp::services::Service; + /// let service = Service::new("AVTransport".to_string()); + /// assert_eq!( + /// service.service_type(), + /// "urn:schemas-upnp-org:service:AVTransport:1" + /// ); + /// ``` + pub fn service_type(&self) -> String { + format!( + "urn:schemas-upnp-org:service:{}:{}", + self.name(), + self.version + ) + } + + /// Retourne l'ideintifiant du service UPnP. + /// + /// Format: `urn:schemas-upnp-org:serviceId:{name}` + /// + /// # Examples + /// + /// ```rust + /// # use pmoupnp::services::Service; + /// let service = Service::new("AVTransport".to_string()); + /// assert_eq!( + /// service.service_id(), + /// "urn:schemas-upnp-org:serviceId:AVTransport" + /// ); + /// ``` + pub fn service_id(&self) -> String { + format!("urn:schemas-upnp-org:serviceId:{}", self.name()) + } + + fn service_base_url(&self) -> String { + format!("/service/{}", self.name()) + } + + pub fn scpd_url(&self) -> String { + format!("{}/desc.xml", self.service_base_url()) + } + + pub fn control_url(&self) -> String { + format!("{}/control", self.service_base_url()) + } + + pub fn event_url(&self) -> String { + format!("{}/event", self.service_base_url()) + } + + pub fn scpd_element(&self) -> Element { + let mut scpd = Element::new("scpd"); + scpd.attributes.insert( + "xmlns".to_string(), + "urn:schemas-upnp-org:service-1-0".to_string(), + ); + + let mut specversion = Element::new("specVersion"); + let mut major= Element::new("major"); + major.children + .push(XMLNode::Text("1".to_string())); + specversion.children.push(XMLNode::Element(major)); + let mut minor = Element::new("minor"); + minor.children + .push(XMLNode::Text("0".to_string())); + specversion.children.push(XMLNode::Element(minor)); + scpd.children.push(XMLNode::Element(specversion)); + + scpd.children.push(XMLNode::Element(self.actions.to_xml_element())); + scpd.children.push(XMLNode::Element(self.state_table.to_xml_element())); + + scpd + } + + pub fn scpd_xml(&self) -> String { + let elem = self.scpd_element(); + + let config = EmitterConfig::new() + .perform_indent(true) + .indent_string(" "); + + let mut buf = Vec::new(); + elem.write_with_config(&mut buf, config) + .expect("Failed to write XML"); + + let mut xml_string = "\n".to_string(); + xml_string.push_str(&String::from_utf8(buf).expect("Invalid UTF-8")); + + xml_string + + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::actions::Action; + use crate::state_variables::StateVariable; + use crate::variable_types::StateVarType; + + #[test] + fn test_service_new() { + let service = Service::new("AVTransport".to_string()); + assert_eq!(service.name(), "AVTransport"); + assert_eq!(service.type_id(), "Service"); + assert_eq!(service.version(), 1); + assert_eq!(service.identifier(), "AVTransport"); + } + + #[test] + fn test_service_set_version() { + let mut service = Service::new("AVTransport".to_string()); + assert!(service.set_version(2).is_ok()); + assert_eq!(service.version(), 2); + + // Version 0 devrait échouer + assert!(service.set_version(0).is_err()); + } + + #[test] + fn test_service_add_variable() { + let mut service = Service::new("AVTransport".to_string()); + let var = Arc::new(StateVariable::new( + StateVarType::String, + "TransportState".to_string(), + )); + + assert!(service.add_variable(var.clone()).is_ok()); + assert!(service.contains_variable(var)); + } + + #[test] + fn test_service_add_action() { + let mut service = Service::new("AVTransport".to_string()); + let action = Arc::new(Action::new("Play".to_string())); + + assert!(service.add_action(action).is_ok()); + assert_eq!(service.actions().len(), 1); + } + + #[test] + fn test_service_type() { + let mut service = Service::new("AVTransport".to_string()); + service.set_version(2).unwrap(); + + assert_eq!( + service.service_type(), + "urn:schemas-upnp-org:service:AVTransport:2" + ); + } +} diff --git a/pmoupnp/src/services/service_instance.rs b/pmoupnp/src/services/service_instance.rs new file mode 100644 index 00000000..c1345ddf --- /dev/null +++ b/pmoupnp/src/services/service_instance.rs @@ -0,0 +1,710 @@ +//! Implémentation de ServiceInstance. + +use std::{ + collections::HashMap, + sync::{Arc, Mutex, RwLock}, + time::Duration, +}; +use axum::{ + extract::{Request, State}, + http::{HeaderMap, StatusCode}, + response::{IntoResponse, Response}, + body::Body, +}; +use tokio::time; +use tracing::{info, warn, error}; +use xmltree::{Element, XMLNode, EmitterConfig}; + +use crate::{ + services::{Service, ServiceError}, + actions::{ActionInstance, ActionInstanceSet}, + state_variables::{StateVarInstance, StateVarInstanceSet, UpnpVariable}, + UpnpObject, UpnpInstance, UpnpTyped, UpnpTypedInstance, UpnpObjectType, +}; + +/// Méthodes HTTP pour les événements UPnP. +pub const METHOD_SUBSCRIBE: &str = "SUBSCRIBE"; +pub const METHOD_UNSUBSCRIBE: &str = "UNSUBSCRIBE"; + +/// Instance de service UPnP. +/// +/// Représente une instance concrète d'un service UPnP, attachée à un device. +/// Gère l'exécution des actions, les notifications d'événements et les abonnements. +/// +/// # Fonctionnalités +/// +/// - Exécution d'actions via SOAP +/// - Gestion des abonnements aux événements (SUBSCRIBE/UNSUBSCRIBE) +/// - Notifications automatiques des changements d'état +/// - Génération de la description SCPD +/// +/// # Cycle de vie +/// +/// 1. Création via [`Service::create_instance`](crate::UpnpModel::create_instance) +/// 2. Enregistrement des URLs avec [`register_urls`](Self::register_urls) +/// 3. Démarrage du notifier avec [`start_notifier`](Self::start_notifier) +/// +/// # Examples +/// +/// ```rust,no_run +/// # use pmoupnp::services::Service; +/// # use pmoupnp::server::Server; +/// # use std::time::Duration; +/// # #[tokio::main] +/// # async fn main() { +/// let service = Service::new("AVTransport".to_string()); +/// let instance = service.create_instance(); +/// +/// // Enregistrer les endpoints +/// let mut server = Server::new("test", "http://localhost:8080", 8080); +/// instance.register_urls(&mut server).await.unwrap(); +/// +/// // Démarrer les notifications +/// let _handle = instance.start_notifier(Duration::from_secs(5)); +/// # } +/// ``` +#[derive(Clone)] +pub struct ServiceInstance { + /// Métadonnées de l'objet + object: UpnpObjectType, + + /// Référence vers le modèle + model: Arc, + + /// Identifiant du service + identifier: String, + + /// Device parent (optionnel) + device: Option>, + + /// Variables d'état instanciées + statevariables: StateVarInstanceSet, + + /// Actions instanciées + actions: ActionInstanceSet, + + /// Abonnés aux événements (SID -> Callback URL) + subscribers: Arc>>, + + /// Buffer des changements en attente de notification + changed_buffer: Arc>>, + + /// Compteurs de séquence par abonné + seqid: Arc>>, +} + +// Stub temporaire pour DeviceInstance +// TODO: Remplacer par la vraie implémentation quand le module devices sera créé +#[derive(Debug, Clone)] +pub struct DeviceStub { + name: String, + udn: String, +} + +impl DeviceStub { + pub fn name(&self) -> &str { + &self.name + } + + pub fn base_route(&self) -> String { + format!("/device/{}", self.name) + } + + pub fn udn(&self) -> &str { + &self.udn + } + + pub fn server_base_url(&self) -> String { + "http://localhost:8080".to_string() + } +} + +impl std::fmt::Debug for ServiceInstance { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ServiceInstance") + .field("object", &self.object) + .field("identifier", &self.identifier) + .field("device", &self.device) + .field("statevariables", &self.statevariables) + .field("actions", &self.actions) + .finish() + } +} + +impl UpnpTyped for ServiceInstance { + fn as_upnp_object_type(&self) -> &UpnpObjectType { + &self.object + } +} + +impl UpnpInstance for ServiceInstance { + type Model = Service; + + fn new(model: &Service) -> Self { + // Phase 1 : Créer les instances de variables d'état + let mut statevariables = StateVarInstanceSet::new(); + for v in model.variables() { + if let Err(e) = statevariables.insert(Arc::new(StateVarInstance::new(&*v))) { + error!("Failed to insert state variable: {:?}", e); + } + } + + // Phase 2 : Créer les instances d'actions avec validation + let mut actions = ActionInstanceSet::new(); + for a in model.actions() { + // Vérifier que toutes les variables référencées existent + let mut missing_vars = Vec::new(); + + for arg in a.arguments().all() { + let related_var_name = arg.state_variable().get_name(); + if statevariables.get_by_name(related_var_name).is_none() { + missing_vars.push(related_var_name.to_string()); + } + } + + if !missing_vars.is_empty() { + error!( + "Action '{}' references missing state variables: {:?}", + a.get_name(), + missing_vars + ); + continue; + } + + // Créer l'instance d'action + let action_instance = Arc::new(ActionInstance::new(&*a)); + + // ✅ Phase 3 : ACTIVER le binding des arguments aux variables d'instance + for arg_instance in action_instance.arguments_set().all() { + let var_name = arg_instance.get_model().state_variable().get_name(); + if let Some(var_instance) = statevariables.get_by_name(var_name) { + // ✅ Activer cette ligne (déjà présente dans ArgumentInstance) + arg_instance.bind_variable(var_instance); + } + } + + if let Err(e) = actions.insert(action_instance) { + error!("Failed to insert action '{}': {:?}", a.get_name(), e); + } + } + + Self { + object: UpnpObjectType { + name: model.name().to_string(), + object_type: "ServiceInstance".to_string(), + }, + model: Arc::new(model.clone()), + identifier: model.identifier().to_string(), + device: None, + statevariables, + actions, + subscribers: Arc::new(RwLock::new(HashMap::new())), + changed_buffer: Arc::new(Mutex::new(HashMap::new())), + seqid: Arc::new(Mutex::new(HashMap::new())), + } + } +} + +impl UpnpTypedInstance for ServiceInstance { + fn get_model(&self) -> &Self::Model { + &self.model + } +} + +impl UpnpObject for ServiceInstance { + fn to_xml_element(&self) -> Element { + let mut elem = Element::new("service"); + + let mut service_type = Element::new("serviceType"); + service_type.children.push(XMLNode::Text(self.service_type())); + elem.children.push(XMLNode::Element(service_type)); + + let mut service_id = Element::new("serviceId"); + service_id.children.push(XMLNode::Text(self.service_id())); + elem.children.push(XMLNode::Element(service_id)); + + let mut scpd_url = Element::new("SCPDURL"); + scpd_url.children.push(XMLNode::Text(self.scpd_url())); + elem.children.push(XMLNode::Element(scpd_url)); + + let mut control_url = Element::new("controlURL"); + control_url.children.push(XMLNode::Text(self.control_url())); + elem.children.push(XMLNode::Element(control_url)); + + let mut event_sub_url = Element::new("eventSubURL"); + event_sub_url.children.push(XMLNode::Text(self.event_sub_url())); + elem.children.push(XMLNode::Element(event_sub_url)); + + elem + } +} + +impl ServiceInstance { + /// Retourne l'identifiant du service. + pub fn identifier(&self) -> &str { + &self.identifier + } + + /// Retourne le type de service UPnP. + /// + /// Format: `urn:schemas-upnp-org:service:{name}:{version}` + pub fn service_type(&self) -> String { + self.model.service_type() + } + + /// Retourne l'ID de service UPnP. + /// + /// Format: `urn:upnp-org:serviceId:{identifier}` + pub fn service_id(&self) -> String { + format!("urn:upnp-org:serviceId:{}", self.identifier) + } + + /// Raccourci pour obtenir une variable d'état par nom + pub fn get_variable(&self, name: &str) -> Option> { + self.statevariables.get_by_name(name) + } + + /// Raccourci pour obtenir une action par nom + pub fn get_action(&self, name: &str) -> Option> { + self.actions.get_by_name(name) + } + + /// Retourne la route de base du service. + pub fn base_route(&self) -> String { + match &self.device { + Some(device) => format!("{}/service/{}", device.base_route(), self.get_name()), + None => format!("/service/{}", self.get_name()), + } + } + + /// Retourne l'URL de contrôle SOAP. + pub fn control_url(&self) -> String { + format!("{}/control", self.base_route()) + } + + /// Retourne l'URL de souscription aux événements. + pub fn event_sub_url(&self) -> String { + format!("{}/event", self.base_route()) + } + + /// Retourne l'URL de la description SCPD. + pub fn scpd_url(&self) -> String { + format!("{}/desc.xml", self.base_route()) + } + + /// Retourne l'USN (Unique Service Name). + pub fn usn(&self) -> String { + match &self.device { + Some(device) => format!("uuid:{}::urn:{}", device.udn(), self.service_type()), + None => format!("uuid::urn:{}", self.service_type()), + } + } + + /// Retourne les variables d'état. + pub fn statevariables(&self) -> &StateVarInstanceSet { + &self.statevariables + } + + /// Retourne les actions. + pub fn actions(&self) -> &ActionInstanceSet { + &self.actions + } + + /// Enregistre les routes UPnP dans le serveur Axum. + /// + /// # Errors + /// + /// Retourne une erreur si l'enregistrement des routes échoue. + pub async fn register_urls(&self, server: &mut crate::server::Server) -> Result<(), ServiceError> { + info!( + "✅ Service description for {}:{} available at : {}{}", + self.device.as_ref().map(|d| d.name()).unwrap_or("unknown"), + self.get_name(), + self.device.as_ref().map(|d| d.server_base_url()).unwrap_or_default(), + self.scpd_url(), + ); + + // Handler SCPD + let instance_scpd = self.clone(); + server.add_handler(&self.scpd_url(), move || { + let instance = instance_scpd.clone(); + async move { instance.scpd_handler().await } + }).await; + + // Handler control + let instance_control = self.clone(); + server.add_post_handler_with_state( + &self.control_url(), + control_handler, + instance_control, + ).await; + + // Handler événements + let instance_event = self.clone(); + server.add_handler_with_state( + &self.event_sub_url(), + event_sub_handler, + instance_event, + ).await; + + Ok(()) + } + + /// Génère l'élément XML SCPD. + pub fn scpd_element(&self) -> Element { + let mut elem = Element::new("scpd"); + elem.attributes.insert( + "xmlns".to_string(), + "urn:schemas-upnp-org:service-1-0".to_string(), + ); + + // specVersion + let mut spec = Element::new("specVersion"); + let mut major = Element::new("major"); + major.children.push(XMLNode::Text("1".to_string())); + spec.children.push(XMLNode::Element(major)); + + let mut minor = Element::new("minor"); + minor.children.push(XMLNode::Text("0".to_string())); + spec.children.push(XMLNode::Element(minor)); + + elem.children.push(XMLNode::Element(spec)); + + // actionList + if !self.actions.all().is_empty() { + elem.children.push(XMLNode::Element( + self.actions.to_xml_element() + )); + } + + // serviceStateTable + if !self.statevariables.all().is_empty() { + elem.children.push(XMLNode::Element( + self.statevariables.to_xml_element() + )); + } + + elem + } + + /// Handler pour la description SCPD. + async fn scpd_handler(&self) -> Response { + let elem = self.scpd_element(); + + let config = EmitterConfig::new() + .perform_indent(true) + .indent_string(" "); + + let mut xml_output = Vec::new(); + if let Err(e) = elem.write_with_config(&mut xml_output, config) { + error!("Failed to serialize SCPD XML: {}", e); + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + + let mut xml = String::from_utf8_lossy(&xml_output).to_string(); + + // Ajouter l'en-tête XML + xml.insert_str(0, "\n"); + + ( + StatusCode::OK, + [(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")], + xml, + ).into_response() + } + + /// Ajoute un abonné aux événements. + pub async fn add_subscriber(&self, sid: String, callback: String) { + let mut subscribers = self.subscribers.write().unwrap(); + subscribers.insert(sid, callback); + } + + /// Renouvelle un abonnement. + pub async fn renew_subscriber(&self, sid: &str, timeout: &str) { + info!("♻️ Renewed SID {} for timeout {}", sid, timeout); + } + + /// Supprime un abonné. + pub async fn remove_subscriber(&self, sid: &str) { + let mut subscribers = self.subscribers.write().unwrap(); + subscribers.remove(sid); + } + + /// Envoie l'événement initial à un nouvel abonné. + pub async fn send_initial_event(&self, sid: String) { + let callback = { + let subscribers = self.subscribers.read().unwrap(); + subscribers.get(&sid).cloned() + }; + + if let Some(callback) = callback { + let mut changed = HashMap::new(); + for sv in self.statevariables.all() { + if sv.is_sending_notification() { + changed.insert(sv.get_name().to_string(), sv.value().to_string()); + } + } + + if changed.is_empty() { + return; + } + + tokio::spawn(async move { + let callback = callback.trim().trim_matches(|c| c == '<' || c == '>'); + + let mut body = r#""#.to_string(); + for (name, val) in changed { + body.push_str(&format!("<{0}>{1}", name, val)); + } + body.push_str(""); + + let client = reqwest::Client::new(); + match client + .request(reqwest::Method::from_bytes(b"NOTIFY").unwrap(), callback) + .header("Content-Type", r#"text/xml; charset="utf-8"#) + .header("NT", "upnp:event") + .header("NTS", "upnp:propchange") + .header("SID", &sid) + .header("SEQ", "0") + .body(body) + .send() + .await + { + Ok(resp) => { + info!("✅ Initial event sent to {}, status={}", callback, resp.status()); + } + Err(e) => { + error!("Failed to send initial event to {}: {}", callback, e); + } + } + }); + } + } + + /// Marque un changement à notifier. + pub fn event_to_be_sent(&self, name: String, value: String) { + let mut buffer = self.changed_buffer.lock().unwrap(); + buffer.insert(name, value); + } + + /// Récupère le prochain numéro de séquence pour un abonné. + fn next_seq(&self, sid: &str) -> String { + let mut seqid = self.seqid.lock().unwrap(); + let counter = seqid.entry(sid.to_string()).or_insert(0); + *counter += 1; + counter.to_string() + } + + /// Notifie tous les abonnés des changements. + pub async fn notify_subscribers(&self) { + let subscribers_copy = { + let subscribers = self.subscribers.read().unwrap(); + if subscribers.is_empty() { + return; + } + subscribers.clone() + }; + + let changed = { + let mut buffer = self.changed_buffer.lock().unwrap(); + if buffer.is_empty() { + return; + } + std::mem::take(&mut *buffer) + }; + + for (sid, callback) in subscribers_copy { + let changed_clone = changed.clone(); + let seq = self.next_seq(&sid); + + tokio::spawn(async move { + let callback = callback.trim().trim_matches(|c| c == '<' || c == '>'); + + let mut body = r#""#.to_string(); + for (name, val) in changed_clone { + body.push_str(&format!("<{0}>{1}", name, val)); + } + body.push_str(""); + + let client = reqwest::Client::new(); + match client + .request(reqwest::Method::from_bytes(b"NOTIFY").unwrap(), callback) + .header("Content-Type", r#"text/xml; charset="utf-8"#) + .header("NT", "upnp:event") + .header("NTS", "upnp:propchange") + .header("SID", &sid) + .header("SEQ", seq) + .body(body) + .send() + .await + { + Ok(_) => { + info!("✅ Notified subscriber {} of changes", callback); + } + Err(e) => { + error!("Failed to notify subscriber {}: {}", callback, e); + } + } + }); + } + } + + /// Démarre le notifier périodique. + /// + /// # Arguments + /// + /// * `interval` - Intervalle entre les notifications + /// + /// # Returns + /// + /// Un handle vers la tâche tokio du notifier. + pub fn start_notifier(&self, interval: Duration) -> tokio::task::JoinHandle<()> { + let instance = self.clone(); + + tokio::spawn(async move { + let mut ticker = time::interval(interval); + info!("✅ Starting notifier every {:?}", interval); + + loop { + ticker.tick().await; + instance.notify_subscribers().await; + } + }) + } +} + +/// Handler Axum pour les événements (SUBSCRIBE/UNSUBSCRIBE). +async fn event_sub_handler( + State(instance): State, + headers: HeaderMap, + req: Request, +) -> Response { + info!("📡 Event Subscription request for {}", instance.get_name()); + + let method = req.method().as_str(); + let sid = headers.get("SID").and_then(|v| v.to_str().ok()).unwrap_or(""); + let timeout = headers.get("Timeout").and_then(|v| v.to_str().ok()).unwrap_or(""); + let callback = headers.get("Callback").and_then(|v| v.to_str().ok()).unwrap_or(""); + + match method { + METHOD_SUBSCRIBE => { + let (response_sid, response_timeout) = if sid.is_empty() { + // Nouvelle souscription + let new_sid = format!("uuid:{}", uuid::Uuid::new_v4()); + if !callback.is_empty() { + instance.add_subscriber(new_sid.clone(), callback.to_string()).await; + } + let timeout_val = if timeout.is_empty() { + "Second-1800" + } else { + timeout + }; + info!("🔒 New subscription: SID={}, Callback={}, Timeout={}", new_sid, callback, timeout_val); + + let sid_clone = new_sid.clone(); + let instance_clone = instance.clone(); + tokio::spawn(async move { + instance_clone.send_initial_event(sid_clone).await; + }); + + (new_sid, timeout_val.to_string()) + } else { + // Renouvellement + instance.renew_subscriber(sid, timeout).await; + info!("♻️ Renew subscription: SID={}, Timeout={}", sid, timeout); + (sid.to_string(), timeout.to_string()) + }; + + ( + StatusCode::OK, + [ + ( + axum::http::header::HeaderName::from_static("sid"), + axum::http::HeaderValue::from_str(&response_sid).unwrap() + ), + ( + axum::http::header::HeaderName::from_static("timeout"), + axum::http::HeaderValue::from_str(&response_timeout).unwrap() + ), + ], + ).into_response() + } + METHOD_UNSUBSCRIBE => { + if !sid.is_empty() { + instance.remove_subscriber(sid).await; + info!("❌ Unsubscribe SID={}", sid); + } + StatusCode::OK.into_response() + } + _ => { + warn!("Unsupported EventSub method: {}", method); + StatusCode::METHOD_NOT_ALLOWED.into_response() + } + } +} + +/// Handler Axum pour le contrôle SOAP. +async fn control_handler( + State(instance): State, + body: String, +) -> Response { + info!("📡 Control request for {}", instance.get_name()); + + // TODO: Parser le SOAP et appeler l'action correspondante + + let response_xml = format!( + r#" + + + + + +"#, + instance.service_type() + ); + + ( + StatusCode::OK, + [(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")], + response_xml, + ).into_response() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::services::Service; + + #[test] + fn test_service_instance_creation() { + let service = Service::new("AVTransport".to_string()); + let instance = ServiceInstance::new(&service); + + assert_eq!(instance.get_name(), "AVTransport"); + assert_eq!(instance.identifier(), "AVTransport"); + } + + #[test] + fn test_service_urls() { + let service = Service::new("AVTransport".to_string()); + let instance = ServiceInstance::new(&service); + + assert_eq!(instance.base_route(), "/service/AVTransport"); + assert_eq!(instance.control_url(), "/service/AVTransport/control"); + assert_eq!(instance.event_sub_url(), "/service/AVTransport/event"); + assert_eq!(instance.scpd_url(), "/service/AVTransport/desc.xml"); + } + + #[test] + fn test_service_type() { + let mut service = Service::new("AVTransport".to_string()); + service.set_version(2).unwrap(); + let instance = ServiceInstance::new(&service); + + assert_eq!( + instance.service_type(), + "urn:schemas-upnp-org:service:AVTransport:2" + ); + } +} \ No newline at end of file diff --git a/pmoupnp/src/services/service_methods.rs b/pmoupnp/src/services/service_methods.rs new file mode 100644 index 00000000..ec2c88a7 --- /dev/null +++ b/pmoupnp/src/services/service_methods.rs @@ -0,0 +1,57 @@ +//! Implémentation des traits UPnP pour Service. + +use xmltree::{Element, XMLNode}; + +use crate::{ + services::{Service, ServiceInstance}, + UpnpObject, UpnpModel, UpnpTyped, UpnpObjectType, +}; + +impl std::fmt::Display for Service { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "Service({}:{})", self.name(), self.version()) + } +} + +impl UpnpTyped for Service { + fn as_upnp_object_type(&self) -> &UpnpObjectType { + &self.object + } +} + +impl UpnpObject for Service { + fn to_xml_element(&self) -> Element { + let mut elem = Element::new("service"); + + // serviceType + let mut service_type = Element::new("serviceType"); + service_type.children.push(XMLNode::Text(self.service_type())); + elem.children.push(XMLNode::Element(service_type)); + + // serviceId + let mut service_id = Element::new("serviceId"); + service_id.children.push(XMLNode::Text(self.service_id())); + elem.children.push(XMLNode::Element(service_id)); + + // SCPDURL + let mut SCPDURL = Element::new("SCPDURL"); + SCPDURL.children.push(XMLNode::Text(self.scpd_url())); + elem.children.push(XMLNode::Element(SCPDURL)); + + // controlURL + let mut controlURL = Element::new("controlURL"); + controlURL.children.push(XMLNode::Text(self.control_url())); + elem.children.push(XMLNode::Element(controlURL)); + + // SCPDURL + let mut eventSubURL = Element::new("eventSubURL"); + eventSubURL.children.push(XMLNode::Text(self.event_url())); + elem.children.push(XMLNode::Element(eventSubURL)); + + elem + } +} + +impl UpnpModel for Service { + type Instance = ServiceInstance; +} \ No newline at end of file diff --git a/pmoupnp/src/state_variables/errors.rs b/pmoupnp/src/state_variables/errors.rs new file mode 100644 index 00000000..050d4e9a --- /dev/null +++ b/pmoupnp/src/state_variables/errors.rs @@ -0,0 +1,28 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum StateVariableError { + #[error("Conversion error: {0}")] + ConversionError(String), + + #[error("Validation error: {0}")] + ValidationError(String), + + #[error("Range error: {0}")] + RangeError(String), + + #[error("Type error: {0}")] + TypeError(String), + + #[error("Parse error: {0}")] + ParseError(String), + + #[error("Event condition error: {0}")] + EventConditionError(String), + + #[error("Arithmetic error: {0}")] + ArithmeticError(String), + + #[error("Unknown error: {0}")] + Unknown(String), +} diff --git a/pmoupnp/src/state_variables/instance_methods.rs b/pmoupnp/src/state_variables/instance_methods.rs new file mode 100644 index 00000000..46b74d40 --- /dev/null +++ b/pmoupnp/src/state_variables/instance_methods.rs @@ -0,0 +1,119 @@ +use std::fmt; + +use chrono::{DateTime, Utc}; +use std::sync::RwLock; +use xmltree::Element; + +use crate::{ + object_trait::{UpnpInstance, UpnpObject}, + state_variables::{StateVarInstance, StateVariable, UpnpVariable}, + variable_types::{StateValue, StateValueError, UpnpVarType}, + UpnpObjectType, UpnpTyped, UpnpTypedInstance +}; + +impl UpnpVariable for StateVarInstance { + fn get_definition(&self) -> &StateVariable { + return &self.model; + } +} + +impl UpnpObject for StateVarInstance { + fn to_xml_element(&self) -> Element { + self.get_definition().to_xml_element() + } +} + +impl UpnpVarType for StateVarInstance { + fn as_state_var_type(&self) -> crate::variable_types::StateVarType { + self.get_definition().as_state_var_type() + } +} + +impl UpnpInstance for StateVarInstance { + type Model = StateVariable; + + fn new(from: &StateVariable) -> Self { + Self { + object: UpnpObjectType { + name: from.object.name.clone(), + object_type: "StateVarInstance".to_string(), + }, + model: from.clone(), + value: RwLock::new(from.get_default()), + old_value: RwLock::new(from.get_default()), + last_modified: RwLock::new(Utc::now()), + last_notification: RwLock::new(Utc::now()), + } + } + +} + +impl UpnpTyped for StateVarInstance { + fn as_upnp_object_type(&self) -> &UpnpObjectType { + return &self.object; + } +} + +impl UpnpTypedInstance for StateVarInstance { + + fn get_model(&self) -> &Self::Model { + &self.model + } +} + +impl fmt::Debug for StateVarInstance { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("StateVarInstance") + .field("object", &self.object) + .field("model", &self.model) + .field("value", &self.value) + .field("old_value", &self.old_value) + .field("last_modified", &self.last_modified) + .field("last_notification", &self.last_notification) + .finish() + } +} + +impl Clone for StateVarInstance { + fn clone(&self) -> Self { + Self { + object: self.object.clone(), + model: self.model.clone(), + value: RwLock::new(self.value.read().unwrap().clone()), + old_value: RwLock::new(self.old_value.read().unwrap().clone()), + last_modified: RwLock::new(self.last_modified.read().unwrap().clone()), + last_notification: RwLock::new(self.last_notification.read().unwrap().clone()), + } + } +} + +impl StateVarInstance { + pub async fn set_value(&self, new_value: StateValue) -> Result<(), StateValueError> { + // Validation du type + if self.as_state_var_type() != new_value.as_state_var_type() { + return Err(StateValueError::TypeError( + "Value type mismatch".to_string() + )); + } + + // Mise à jour avec les locks + let mut old_val = self.old_value.write().unwrap(); + let mut val = self.value.write().unwrap(); + let mut modified = self.last_modified.write().unwrap(); + + *old_val = val.clone(); + *val = new_value; + *modified = Utc::now(); + + Ok(()) + } + /// Accès à la valeur + pub fn value(&self) -> StateValue { + self.value.read().unwrap().clone() + } + + /// Accès au timestamp + pub fn last_modified(&self) -> DateTime { + self.last_modified.read().unwrap().clone() + } +} diff --git a/pmoupnp/src/state_variables/macros.rs b/pmoupnp/src/state_variables/macros.rs new file mode 100644 index 00000000..4afb69c5 --- /dev/null +++ b/pmoupnp/src/state_variables/macros.rs @@ -0,0 +1,172 @@ +/// Macro pour définir facilement une variable d'état UPnP. +/// +/// Cette macro simplifie la création de variables d'état UPnP statiques en générant +/// automatiquement le code nécessaire pour initialiser une variable avec ses propriétés. +/// +/// # Syntaxe +/// +/// ## Variable simple (sans valeurs autorisées) +/// +/// ```ignore +/// define_variable! { +/// pub static VAR_NAME: Type = "VariableName" +/// } +/// ``` +/// +/// ## Variable avec valeurs autorisées (enum) +/// +/// ```ignore +/// define_variable! { +/// pub static VAR_NAME: String = "VariableName" { +/// allowed: ["Value1", "Value2", "Value3"], +/// } +/// } +/// ``` +/// +/// ## Variable avec valeur par défaut +/// +/// ```ignore +/// define_variable! { +/// pub static VAR_NAME: String = "VariableName" { +/// allowed: ["Value1", "Value2"], +/// default: "Value1", +/// } +/// } +/// ``` +/// +/// ## Variable avec notification d'événements +/// +/// ```ignore +/// define_variable! { +/// pub static VAR_NAME: String = "VariableName" { +/// allowed: ["STOPPED", "PLAYING"], +/// evented: true, +/// } +/// } +/// ``` +/// +/// ## Variable avec toutes les options +/// +/// ```ignore +/// define_variable! { +/// pub static VAR_NAME: String = "VariableName" { +/// allowed: ["Value1", "Value2", "Value3"], +/// default: "Value1", +/// evented: true, +/// } +/// } +/// ``` +/// +/// # Types supportés +/// +/// - `String` : Chaîne de caractères +/// - `UI1`, `UI2`, `UI4` : Entiers non signés +/// - `I1`, `I2`, `I4` : Entiers signés +/// - `Boolean` : Booléen +/// +/// # Arguments optionnels +/// +/// - `allowed: [...]` : Liste des valeurs autorisées (enum) +/// - `default: "..."` : Valeur par défaut +/// - `evented: true` : Active les notifications d'événements +/// +/// # Examples +/// +/// ```ignore +/// use pmoupnp::define_variable; +/// +/// // Variable simple +/// define_variable! { +/// pub static VOLUME: UI2 = "Volume" +/// } +/// +/// // Variable avec enum +/// define_variable! { +/// pub static MUTE: String = "Mute" { +/// allowed: ["0", "1"], +/// default: "0", +/// } +/// } +/// +/// // Variable avec notification +/// define_variable! { +/// pub static TRANSPORT_STATE: String = "TransportState" { +/// allowed: ["STOPPED", "PLAYING", "PAUSED_PLAYBACK"], +/// evented: true, +/// } +/// } +/// ``` +#[macro_export] +macro_rules! define_variable { + // Variable simple sans options + (pub static $name:ident: $type:ident = $var_name:literal) => { + pub static $name: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| { + let sv = $crate::state_variables::StateVariable::new( + $crate::variable_types::StateVarType::$type, + $var_name.to_string() + ); + std::sync::Arc::new(sv) + }); + }; + + // Variable avec options + (pub static $name:ident: $type:ident = $var_name:literal { + $(allowed: [$($value:literal),* $(,)?],)? + $(default: $default:literal,)? + $(evented: $evented:literal,)? + }) => { + pub static $name: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| { + let mut sv = $crate::state_variables::StateVariable::new( + $crate::variable_types::StateVarType::$type, + $var_name.to_string() + ); + + $( + sv.extend_allowed_values(&[ + $(define_variable!(@value $type, $value)),* + ]).expect(&format!("Cannot set allowed values for {}", $var_name)); + )? + + $( + sv.set_default(&define_variable!(@value $type, $default)) + .expect(&format!("Cannot set default value for {}", $var_name)); + )? + + $( + if $evented { + sv.set_send_notification(); + } + )? + + std::sync::Arc::new(sv) + }); + }; + + // Helper pour créer une StateValue selon le type + (@value String, $val:literal) => { + $crate::variable_types::StateValue::String($val.to_string()) + }; + (@value UI1, $val:literal) => { + $crate::variable_types::StateValue::UI1($val) + }; + (@value UI2, $val:literal) => { + $crate::variable_types::StateValue::UI2($val) + }; + (@value UI4, $val:literal) => { + $crate::variable_types::StateValue::UI4($val) + }; + (@value I1, $val:literal) => { + $crate::variable_types::StateValue::I1($val) + }; + (@value I2, $val:literal) => { + $crate::variable_types::StateValue::I2($val) + }; + (@value I4, $val:literal) => { + $crate::variable_types::StateValue::I4($val) + }; + (@value Boolean, $val:literal) => { + $crate::variable_types::StateValue::Boolean($val) + }; +} diff --git a/pmoupnp/src/state_variables/mod.rs b/pmoupnp/src/state_variables/mod.rs new file mode 100644 index 00000000..137d1e45 --- /dev/null +++ b/pmoupnp/src/state_variables/mod.rs @@ -0,0 +1,64 @@ +mod errors; +mod instance_methods; +mod macros; +mod variable_methods; +mod var_set_methods; +mod var_inst_set_methods; +mod variable_trait; + +use std::{ + collections::HashMap, + sync::Arc, +}; + +pub use crate::state_variables::variable_trait::UpnpVariable; +use bevy_reflect::Reflect; +use chrono::{DateTime, Utc}; +pub use errors::StateVariableError; +use std::sync::RwLock; + +use crate::{ + value_ranges::ValueRange, + variable_types::{StateValue, StateVarType}, + UpnpObjectSet, UpnpObjectType, +}; + +/// Type pour les fonctions de condition d'événement +pub type StateConditionFunc = Arc bool + Send + Sync>; + +/// Type pour les fonctions de parsing de valeurs depuis des chaînes +pub type StringValueParser = + Arc Result, StateVariableError> + Send + Sync>; + +/// Type pour les fonctions de sérialisation de valeurs vers des chaînes +pub type ValueSerializer = + Arc Result + Send + Sync>; + +pub struct StateVariable { + object: UpnpObjectType, + value_type: StateVarType, + step: Option, + modifiable: bool, + event_conditions: Arc>>, + description: String, + default_value: Option, + value_range: Option, + allowed_values: Arc>>, + send_events: bool, + parse: Option, + marshal: Option, +} + +pub type StateVariableSet = UpnpObjectSet; + +pub struct StateVarInstance { + object: UpnpObjectType, + model: StateVariable, + value: RwLock, + old_value: RwLock, + last_modified: RwLock>, + last_notification: RwLock>, +} + +pub type StateVarInstanceSet = UpnpObjectSet; + diff --git a/pmoupnp/src/state_variables/var_inst_set_methods.rs b/pmoupnp/src/state_variables/var_inst_set_methods.rs new file mode 100644 index 00000000..e5d55dc3 --- /dev/null +++ b/pmoupnp/src/state_variables/var_inst_set_methods.rs @@ -0,0 +1,33 @@ +use std::collections::HashMap; + +use std::sync::RwLock; +use xmltree::{Element, XMLNode}; + +use crate::{state_variables::{StateVarInstanceSet, StateVariableSet}, UpnpObject}; + +use crate::UpnpInstance; + +impl UpnpObject for StateVarInstanceSet { + fn to_xml_element(&self) -> Element { + let mut elem = Element::new("serviceStateTable"); + + for state_var in self.all() { + let state_var_elem = state_var.to_xml_element(); // retourne un complet + elem.children.push(XMLNode::Element(state_var_elem)); + } + + elem + } +} + +impl UpnpInstance for StateVarInstanceSet { + type Model = StateVariableSet; + + fn new(_: &StateVariableSet) -> Self { + Self { objects: RwLock::new(HashMap::new()) } + } + + +} + + diff --git a/pmoupnp/src/state_variables/var_set_methods.rs b/pmoupnp/src/state_variables/var_set_methods.rs new file mode 100644 index 00000000..14b8c836 --- /dev/null +++ b/pmoupnp/src/state_variables/var_set_methods.rs @@ -0,0 +1,25 @@ +use xmltree::{Element, XMLNode}; + +use crate::{object_trait::UpnpModel, state_variables::{StateVarInstanceSet, StateVariableSet}, UpnpObject}; + + +impl UpnpObject for StateVariableSet { + + fn to_xml_element(&self) -> Element { + let mut elem = Element::new("serviceStateTable"); + + for state_var in self.all() { + let state_var_elem = state_var.to_xml_element(); // retourne un complet + elem.children.push(XMLNode::Element(state_var_elem)); + } + + elem + } + +} + +impl UpnpModel for StateVariableSet { + type Instance = StateVarInstanceSet; +} + + diff --git a/pmoupnp/src/state_variables/variable_methods.rs b/pmoupnp/src/state_variables/variable_methods.rs new file mode 100644 index 00000000..3bda6759 --- /dev/null +++ b/pmoupnp/src/state_variables/variable_methods.rs @@ -0,0 +1,406 @@ +use std::{ + collections::HashMap, + fmt, + sync::Arc, +}; + +use std::sync::RwLock; +use xmltree::{Element, XMLNode}; + +use crate::{ + UpnpObjectType, UpnpTyped, + object_trait::{UpnpModel, UpnpObject}, + state_variables::{ + StateConditionFunc, StateVarInstance, StateVariable, StringValueParser, ValueSerializer, + variable_trait::UpnpVariable, + }, + value_ranges::ValueRange, + variable_types::{StateValue, StateValueError, StateVarType, UpnpVarType}, +}; + +impl UpnpTyped for StateVariable { + fn as_upnp_object_type(&self) -> &UpnpObjectType { + &self.object + } +} + +impl UpnpVarType for StateVariable { + fn as_state_var_type(&self) -> StateVarType { + self.value_type.as_state_var_type() // utilise ton From<&StateValue> existant + } +} + +impl UpnpObject for StateVariable { + fn to_xml_element(&self) -> Element { + // Création de l'élément racine + let mut root = Element::new("stateVariable"); + root.attributes.insert( + "sendEvents".to_string(), + if self.send_events { "yes" } else { "no" }.to_string(), + ); + + // + let mut name_elem = Element::new("name"); + name_elem + .children + .push(XMLNode::Text(self.get_name().clone())); + + // + let mut datatype_elem = Element::new("dataType"); + datatype_elem + .children + .push(XMLNode::Text(self.value_type.to_string())); // StateVarType doit impl Display + + // si défini + if let Some(default) = &self.default_value { + let mut def_elem = Element::new("defaultValue"); + def_elem.children.push(XMLNode::Text(default.to_string())); + root.children.push(XMLNode::Element(def_elem)); + } + + // si défini + let av = self.allowed_values.read().unwrap(); + if !av.is_empty() { + let mut list_elem = Element::new("allowedValueList"); + for val in av.iter() { + let mut val_elem = Element::new("allowedValue"); + val_elem.children.push(XMLNode::Text(val.to_string())); + list_elem.children.push(XMLNode::Element(val_elem)); + } + root.children.push(XMLNode::Element(list_elem)); + } + + // si défini + if let Some(range) = &self.value_range { + let mut range_elem = Element::new("allowedValueRange"); + + let mut min_elem = Element::new("minimum"); + min_elem + .children + .push(XMLNode::Text(range.get_minimum().to_string())); + range_elem.children.push(XMLNode::Element(min_elem)); + + let mut max_elem = Element::new("maximum"); + max_elem + .children + .push(XMLNode::Text(range.get_maximum().to_string())); + range_elem.children.push(XMLNode::Element(max_elem)); + + if let Some(step) = &self.step { + let mut step_elem = Element::new("step"); + step_elem.children.push(XMLNode::Text(step.to_string())); + range_elem.children.push(XMLNode::Element(step_elem)); + } + + root.children.push(XMLNode::Element(range_elem)); + } + + // Ajouter les enfants communs + root.children.push(XMLNode::Element(name_elem)); + root.children.push(XMLNode::Element(datatype_elem)); + + root + } +} + +impl UpnpModel for StateVariable { + type Instance = StateVarInstance; +} + +impl Clone for StateVariable { + fn clone(&self) -> Self { + // clone safe des structures protégées par RwLock en prenant un read lock + let event_conditions_clone = { + // si le lock est "poisoned" on panic - tu peux adapter la gestion si tu veux + let guard = self + .event_conditions + .read().unwrap(); + // nécessite que Key: Clone, Value: Clone + Arc::new(RwLock::new(guard.clone())) + }; + + let allowed_values_clone = { + let guard = self + .allowed_values + .read().unwrap(); + Arc::new(RwLock::new(guard.clone())) + }; + + Self { + object: self.object.clone(), + value_type: self.value_type.clone(), + step: self.step.clone(), + modifiable: self.modifiable, + event_conditions: event_conditions_clone, + description: self.description.clone(), + default_value: self.default_value.clone(), + value_range: self.value_range.clone(), + allowed_values: allowed_values_clone, + send_events: self.send_events, + // parse et marshal sont typiquement des Arc — on clone l'Arc (shallow). + // Deep-cloner une closure ou un trait-objet n'est pas possible en général. + parse: self.parse.clone(), + marshal: self.marshal.clone(), + } + } +} + +impl fmt::Debug for StateVariable { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("StateVariable") + .field("object", &self.object) + .field("value_type", &self.value_type) + .field("step", &self.step) + .field("modifiable", &self.modifiable) + .field( + "event_conditions", + &format_args!( + "len={}", + self.event_conditions.read().unwrap().len() + ), + ) + .field("description", &self.description) + .field("default_value", &self.default_value) + .field("value_range", &self.value_range) + .field( + "allowed_values", + &format_args!( + "len={}", + self.allowed_values.read().unwrap().len() + ), + ) + .field("send_events", &self.send_events) + .field( + "parse", + &self + .parse + .as_ref() + .map(|_| "Some(StringValueParser)") + .unwrap_or("None"), + ) + .field( + "marshal", + &self + .marshal + .as_ref() + .map(|_| "Some(ValueSerializer)") + .unwrap_or("None"), + ) + .finish() + } +} + +impl UpnpVariable for StateVariable { + fn get_definition(&self) -> &StateVariable { + return self; + } +} + +impl StateVariable { + pub fn new(vartype: StateVarType, name: String) -> StateVariable { + Self { + object: UpnpObjectType { + name, + object_type: "StateVariable".to_string(), + }, + value_type: vartype.clone(), + step: None, + modifiable: true, + event_conditions: Arc::new(RwLock::new(HashMap::new())), + description: "".to_string(), + default_value: None, + value_range: None, + allowed_values: Arc::new(RwLock::new(Vec::new())), + send_events: false, + parse: None, + marshal: None, + } + } + + pub fn set_step(&mut self, step: StateValue) -> Result<(), StateValueError> { + if self.as_state_var_type() != step.as_state_var_type() { + return Err(StateValueError::TypeError("Bad step type".to_string())); + } + + self.step = Some(step); + Ok(()) + } + + pub fn set_range(&mut self, min: &StateValue, max: &StateValue) -> Result<(), StateValueError> { + if self.as_state_var_type() != min.as_state_var_type() { + return Err(StateValueError::TypeError("Bad range type".to_string())); + } + + let range = ValueRange::new(min, max)?; // ? propage l'erreur si elle existe + self.value_range = Some(range); + Ok(()) + } + + pub fn update_minimum(&mut self, min: &StateValue) -> Result<(), StateValueError> { + if !self.has_range() { + return Err(StateValueError::RangeError( + "No range specified for this variable".to_string(), + )); + } + if self + .value_range + .as_ref() + .expect("Range is not defined") + .as_state_var_type() + != min.as_state_var_type() + { + return Err(StateValueError::TypeError( + "new minimum is not the same than state variable".to_string(), + )); + } + self.value_range + .as_mut() + .expect("Range is not defined") + .set_minimum(min); + return Ok(()); + } + + pub fn update_maximum(&mut self, min: &StateValue) -> Result<(), StateValueError> { + if !self.has_range() { + return Err(StateValueError::RangeError( + "No range specified for this variable".to_string(), + )); + } + if self + .value_range + .as_ref() + .expect("Range is not defined") + .as_state_var_type() + != min.as_state_var_type() + { + return Err(StateValueError::TypeError( + "new minimum is not the same than state variable".to_string(), + )); + } + self.value_range + .as_mut() + .expect("Range is not defined") + .set_maximum(min); + return Ok(()); + } + + pub fn get_range(&self) -> Option<&ValueRange> { + return self.value_range.as_ref(); + } + + pub fn set_modifiable(&mut self) { + self.modifiable = true; + } + + pub fn set_not_modifiable(&mut self) { + self.modifiable = false; + } + + pub fn add_event_condition(&self, name: String, func: StateConditionFunc) { + // on lock en écriture + let mut guard = self.event_conditions.write().unwrap(); + guard.insert(name, func); + // le lock est automatiquement relâché ici (RAII) + } + + pub fn remove_event_condition(&self, name: &str) { + let mut guard = self.event_conditions.write().unwrap(); + guard.remove(name); + } + + pub fn clear_event_conditions(&mut self) { + let mut guard = self.event_conditions.write().unwrap(); + guard.clear() + } + + pub fn set_description(&mut self, description: String) { + self.description = description; + } + + 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(), + )); + } + self.default_value = Some(value.clone()); + return Ok(()); + } + + pub fn unset_default(&mut self) { + self.default_value = None; + } + + pub fn extend_allowed_values(&mut self, values: &[StateValue]) -> Result<(), StateValueError> { + let mut av = self + .allowed_values + .write().unwrap(); + + for v in values { + if self.as_state_var_type() == v.as_state_var_type() { + av.push(v.clone()); + } else { + return Err(StateValueError::TypeError( + "new allowed value does not have the right type".to_string(), + )); + } + } + + Ok(()) + } + + pub fn push_allowed_value(&mut self, value: &StateValue) -> Result<(), StateValueError> { + let mut av = self + .allowed_values + .write().unwrap(); + + if self.as_state_var_type() == value.as_state_var_type() { + av.push(value.clone()); + } else { + return Err(StateValueError::TypeError( + "new allowed value does not have the right type".to_string(), + )); + } + + return Ok(()); + } + + pub fn set_send_notification(&mut self) { + self.send_events = true; + } + + pub fn unset_send_notification(&mut self) { + self.send_events = false; + } + + 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(()); + } + return Err(StateValueError::TypeError( + "Only String variables can have a parser".to_string(), + )); + } + + pub fn unset_value_parser(&mut self) { + self.parse = None; + } + + pub fn set_value_marshaler( + &mut self, + marshaler: ValueSerializer, + ) -> Result<(), StateValueError> { + if self.as_state_var_type() == StateVarType::String { + self.marshal = Some(marshaler); + return Ok(()); + } + return Err(StateValueError::TypeError( + "Only String variables can have a marshaler".to_string(), + )); + } + + pub fn unset_value_marshaler(&mut self) { + self.marshal = None; + } +} diff --git a/pmoupnp/src/state_variables/variable_trait.rs b/pmoupnp/src/state_variables/variable_trait.rs new file mode 100644 index 00000000..07b19a48 --- /dev/null +++ b/pmoupnp/src/state_variables/variable_trait.rs @@ -0,0 +1,302 @@ +use crate::{ + state_variables::StateVariable, + variable_types::{StateValue, UpnpVarType}, +}; + +/// Trait pour accéder aux propriétés et contraintes d'une variable UPnP. +/// +/// Ce trait fournit une interface uniforme pour interroger les métadonnées, +/// contraintes et comportements d'une variable UPnP, qu'il s'agisse d'une +/// définition ([`StateVariable`]) ou d'une instance ([`StateVarInstance`]). +/// +/// # Architecture +/// +/// Le trait utilise le pattern "trait avec implémentation par défaut" : +/// - Une seule méthode requise : [`get_definition`](Self::get_definition) +/// - Toutes les autres méthodes sont implémentées par défaut en déléguant à la définition +/// +/// Cela permet une interface cohérente entre modèles et instances sans duplication de code. +/// +/// # Hiérarchie +/// +/// ```text +/// UpnpVariable +/// ├─> StateVariable (get_definition() retourne self) +/// └─> StateVarInstance (get_definition() retourne self.definition) +/// ``` +/// +/// # Examples +/// +/// ```ignore +/// fn display_variable_info(var: &V) { +/// println!("Variable: {}", var.get_definition().get_name()); +/// +/// if var.has_default() { +/// println!("Default: {:?}", var.get_default()); +/// } +/// +/// if var.has_range() { +/// println!("Has range constraints"); +/// } +/// +/// if var.has_allowed_values() { +/// println!("Has allowed values list"); +/// } +/// } +/// ``` +pub trait UpnpVariable { + /// Retourne une référence vers la définition de la variable. + /// + /// Cette méthode est la base de toutes les autres méthodes du trait. + /// + /// # Implementation + /// + /// - Pour [`StateVariable`] : retourne `self` + /// - Pour [`StateVarInstance`] : retourne `self.definition` + fn get_definition(&self) -> &StateVariable; + + /// Indique si la variable a un pas (step) défini. + /// + /// Le pas définit l'incrément minimal entre deux valeurs valides pour + /// les types numériques. + /// + /// # Returns + /// + /// `true` si un pas est défini, `false` sinon. + /// + /// # Examples + /// + /// ```ignore + /// if var.has_step() { + /// println!("Step: {:?}", var.get_step()); + /// } + /// ``` + fn has_step(&self) -> bool { + self.get_definition().step.is_some() + } + + /// Retourne le pas (step) de la variable s'il est défini. + /// + /// # Returns + /// + /// - `Some(StateValue)` si un pas est défini + /// - `None` sinon + /// + /// # See also + /// + /// - [`has_step`](Self::has_step) pour tester l'existence + fn get_step(&self) -> Option { + self.get_definition().step.clone() + } + + /// Indique si la variable a une plage de valeurs (range) définie. + /// + /// La plage définit les valeurs minimale et maximale acceptables. + /// + /// # Returns + /// + /// `true` si une plage est définie, `false` sinon. + fn has_range(&self) -> bool { + self.get_definition().value_range.is_some() + } + + /// Indique si la variable est modifiable. + /// + /// Une variable non modifiable est en lecture seule. + /// + /// # Returns + /// + /// `true` si la variable peut être modifiée, `false` sinon. + fn is_modifiable(&self) -> bool { + self.get_definition().modifiable + } + + /// Indique si la variable a des conditions d'événement définies. + /// + /// Les conditions d'événement déterminent quand des notifications + /// doivent être envoyées lors de changements de valeur. + /// + /// # Returns + /// + /// `true` si au moins une condition d'événement existe, `false` sinon. + /// + /// # Note + /// + /// Retourne `false` si le lock est empoisonné (poisoned). + fn has_event_conditions(&self) -> bool { + let guard = self.get_definition().event_conditions.read().unwrap(); + !guard.is_empty() + } + + /// Vérifie si une condition d'événement spécifique existe. + /// + /// # Arguments + /// + /// * `name` - Le nom de la condition à rechercher + /// + /// # Returns + /// + /// `true` si la condition existe, `false` sinon. + /// + /// # Note + /// + /// Retourne `false` si le lock est empoisonné (poisoned). + fn has_event_condition(&self, name: &String) -> bool { + let guard = self.get_definition().event_conditions.read().unwrap(); + guard.contains_key(name) + } + + /// Indique si la variable a une description non vide. + /// + /// # Returns + /// + /// `true` si une description existe et n'est pas vide, `false` sinon. + fn has_description(&self) -> bool { + !self.get_definition().description.is_empty() + } + + /// Retourne la description de la variable. + /// + /// # Returns + /// + /// La description sous forme de `String`. Peut être vide. + /// + /// # See also + /// + /// - [`has_description`](Self::has_description) pour tester si non vide + fn get_description(&self) -> String { + self.get_definition().description.clone() + } + + /// Indique si la variable a une valeur par défaut définie explicitement. + /// + /// # Returns + /// + /// `true` si une valeur par défaut est explicitement définie, `false` sinon. + /// + /// # Note + /// + /// Même si cette méthode retourne `false`, [`get_default`](Self::get_default) + /// retournera toujours une valeur (la valeur par défaut du type). + fn has_default(&self) -> bool { + self.get_definition().default_value.is_some() + } + + /// Retourne la valeur par défaut de la variable. + /// + /// # Returns + /// + /// La valeur par défaut. Si aucune valeur par défaut n'est explicitement + /// définie, retourne la valeur par défaut du type de la variable + /// (ex: 0 pour les entiers, chaîne vide pour String, etc.). + /// + /// # Examples + /// + /// ```ignore + /// let default = var.get_default(); + /// println!("Default value: {:?}", default); + /// ``` + fn get_default(&self) -> StateValue { + self.get_definition() + .default_value + .clone() + .unwrap_or_else(|| self.get_definition().as_state_var_type().default_value()) + } + + /// Indique si la variable a une liste de valeurs autorisées. + /// + /// Lorsqu'une liste de valeurs autorisées est définie, seules ces valeurs + /// sont acceptables pour la variable. + /// + /// # Returns + /// + /// `true` si une liste non vide de valeurs autorisées existe, `false` sinon. + /// + /// # Note + /// + /// Retourne `false` si le lock est empoisonné (poisoned). + fn has_allowed_values(&self) -> bool { + let guard = self.get_definition() + .allowed_values + .read().unwrap(); + + !guard.is_empty() + } + + /// Vérifie si une valeur fait partie des valeurs autorisées. + /// + /// # Arguments + /// + /// * `value` - La valeur à vérifier + /// + /// # Returns + /// + /// `true` si la valeur est dans la liste des valeurs autorisées, `false` sinon. + /// Retourne également `false` si aucune liste de valeurs autorisées n'est définie + /// ou si le lock est empoisonné. + /// + /// # Examples + /// + /// ```ignore + /// let value = StateValue::String("ON".to_string()); + /// if var.is_an_allowed_value(&value) { + /// println!("Value is allowed"); + /// } + /// ``` + /// + /// # Note + /// + /// Si aucune liste de valeurs autorisées n'est définie, cette méthode + /// retourne `false`. Utilisez [`has_allowed_values`](Self::has_allowed_values) + /// pour distinguer "pas de liste" de "valeur non autorisée". + fn is_an_allowed_value(&self, value: &StateValue) -> bool { + let guard = self.get_definition() + .allowed_values + .read().unwrap(); + + guard.contains(value) + } + + /// Indique si la variable envoie des notifications d'événement. + /// + /// Les notifications d'événement sont envoyées aux abonnés lorsque + /// la valeur de la variable change. + /// + /// # Returns + /// + /// `true` si les notifications sont activées, `false` sinon. + /// + /// # See also + /// + /// - [`has_event_conditions`](Self::has_event_conditions) pour vérifier + /// les conditions d'envoi d'événements + fn is_sending_notification(&self) -> bool { + self.get_definition().send_events + } + + /// Indique si la variable a un parser de valeur personnalisé. + /// + /// Un parser personnalisé est utilisé pour convertir des chaînes de + /// caractères en valeurs typées. Disponible uniquement pour les variables + /// de type String. + /// + /// # Returns + /// + /// `true` si un parser est défini, `false` sinon. + fn has_value_parser(&self) -> bool { + self.get_definition().parse.is_some() + } + + /// Indique si la variable a un marshaler de valeur personnalisé. + /// + /// Un marshaler personnalisé est utilisé pour sérialiser des valeurs + /// en chaînes de caractères. Disponible uniquement pour les variables + /// de type String. + /// + /// # Returns + /// + /// `true` si un marshaler est défini, `false` sinon. + fn has_value_marshaler(&self) -> bool { + self.get_definition().marshal.is_some() + } +} diff --git a/pmoupnp/src/value_ranges/methods.rs b/pmoupnp/src/value_ranges/methods.rs new file mode 100644 index 00000000..cf12f640 --- /dev/null +++ b/pmoupnp/src/value_ranges/methods.rs @@ -0,0 +1,69 @@ +use std::cmp::Ordering; + +use crate::{ + value_ranges::ValueRange, + variable_types::{StateValue, StateValueError, StateVarType, UpnpVarType}, +}; + +impl UpnpVarType for ValueRange { + fn as_state_var_type(&self) -> StateVarType { + self.min.as_state_var_type() // utilise ton From<&StateValue> existant + } +} + +impl ValueRange { + pub fn new(min: &StateValue, max: &StateValue) -> Result { + if min.as_state_var_type() != max.as_state_var_type() { + return Err(StateValueError::TypeError( + "min and max do not belong the same time".to_string(), + )); + } + + // Vérifier que min <= max + if let Some(cmp) = min.partial_cmp(max) { + if cmp == Ordering::Greater { + return Err(StateValueError::RangeError( + "Minimum cannot be greater than maximum".to_string(), + )); + } + } + + Ok(Self { + min: min.clone(), + max: max.clone(), + }) + } + + pub fn get_minimum(self: &ValueRange) -> StateValue { + return self.min.clone(); + } + + pub fn set_minimum(&mut self, value: &StateValue) { + self.min = value.clone() + } + + pub fn get_maximum(self: &ValueRange) -> StateValue { + return self.max.clone(); + } + + pub fn set_maximum(&mut self, value: &StateValue) { + self.max = value.clone() + } + + pub fn is_in_range(&self, value: &StateValue) -> bool { + if self.as_state_var_type() == value.as_state_var_type() + && let Some(cmp) = self.min.partial_cmp(value) + { + if cmp == Ordering::Greater { + return false; + } + if let Some(cmp2) = self.max.partial_cmp(value) { + if cmp2 == Ordering::Less { + return false; + } + return true; + } + } + return false; + } +} diff --git a/pmoupnp/src/value_ranges/mod.rs b/pmoupnp/src/value_ranges/mod.rs new file mode 100644 index 00000000..7eaa0c4d --- /dev/null +++ b/pmoupnp/src/value_ranges/mod.rs @@ -0,0 +1,9 @@ +mod methods; + +use crate::variable_types::StateValue; + +#[derive(Debug, Clone)] +pub struct ValueRange { + min: StateValue, + max: StateValue, +} diff --git a/pmoupnp/src/variable_types/cast.rs b/pmoupnp/src/variable_types/cast.rs new file mode 100644 index 00000000..d9b6cfd3 --- /dev/null +++ b/pmoupnp/src/variable_types/cast.rs @@ -0,0 +1,36 @@ +use crate::variable_types::{StateValue, StateValueError, StateVarType}; +use std::convert::TryFrom; + +impl StateValue { + pub fn try_cast(&self, target: StateVarType) -> Result { + let source = StateVarType::from(self); + + // Identité (même type) + if source == target { + return Ok(self.clone()); + } + + match (self, target) { + (val, StateVarType::String) => Ok(StateValue::String(val.to_string())), + + (_, StateVarType::UI1) => Ok(StateValue::UI1(u8::try_from(self)?)), + (_, StateVarType::UI2) => Ok(StateValue::UI2(u16::try_from(self)?)), + (_, StateVarType::UI4) => Ok(StateValue::UI4(u32::try_from(self)?)), + (_, StateVarType::I1) => Ok(StateValue::I1(i8::try_from(self)?)), + (_, StateVarType::I2) => Ok(StateValue::I2(i16::try_from(self)?)), + (_, StateVarType::I4) => Ok(StateValue::I4(i32::try_from(self)?)), + (_, StateVarType::Int) => Ok(StateValue::Int(i32::try_from(self)?)), + + (_, StateVarType::R8) => Ok(StateValue::R8(f64::try_from(self)?)), + (_, StateVarType::Number) => Ok(StateValue::Number(f64::try_from(self)?)), + (_, StateVarType::Fixed14_4) => Ok(StateValue::Fixed14_4(f64::try_from(self)?)), + (_, StateVarType::R4) => Ok(StateValue::R4(f32::try_from(self)?)), + + // --- Pas encore implémenté pour les autres types --- + (val, target) => Err(StateValueError::TypeError(format!( + "Cannot cast {:?} to {:?}", + val, target + ))), + } + } +} diff --git a/pmoupnp/src/variable_types/default_value.rs b/pmoupnp/src/variable_types/default_value.rs new file mode 100644 index 00000000..10795be8 --- /dev/null +++ b/pmoupnp/src/variable_types/default_value.rs @@ -0,0 +1,45 @@ +use chrono::{DateTime, FixedOffset, NaiveDate, NaiveTime}; +use url::Url; +use uuid::Uuid; + +use crate::variable_types::{StateValue, StateVarType}; + +impl StateVarType { + pub fn default_value(&self) -> StateValue { + match self { + StateVarType::UI1 => StateValue::UI1(0), + StateVarType::UI2 => StateValue::UI2(0), + StateVarType::UI4 => StateValue::UI4(0), + StateVarType::I1 => StateValue::I1(0), + StateVarType::I2 => StateValue::I2(0), + StateVarType::I4 => StateValue::I4(0), + StateVarType::Int => StateValue::Int(0), + StateVarType::R4 => StateValue::R4(0.0), + StateVarType::R8 => StateValue::R8(0.0), + StateVarType::Number => StateValue::Number(0.0), + StateVarType::Fixed14_4 => StateValue::Fixed14_4(0.0), + StateVarType::Char => StateValue::Char('\0'), + StateVarType::String => StateValue::String(String::new()), + StateVarType::Boolean => StateValue::Boolean(false), + StateVarType::BinBase64 => StateValue::BinBase64(String::new()), + StateVarType::BinHex => StateValue::BinHex(String::new()), + StateVarType::Date => StateValue::Date(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()), + StateVarType::DateTime => { + StateValue::DateTime(DateTime::from_timestamp(0, 0).unwrap().naive_utc().into()) + } + StateVarType::DateTimeTZ => { + StateValue::DateTimeTZ(DateTime::from_naive_utc_and_offset( + DateTime::from_timestamp(0, 0).unwrap().naive_utc(), + FixedOffset::east_opt(0).unwrap(), + )) + } + StateVarType::Time => StateValue::Time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()), + StateVarType::TimeTZ => StateValue::TimeTZ(DateTime::from_naive_utc_and_offset( + DateTime::from_timestamp(0, 0).unwrap().naive_utc(), + FixedOffset::east_opt(0).unwrap(), + )), + StateVarType::UUID => StateValue::UUID(Uuid::nil()), + StateVarType::URI => StateValue::URI(Url::parse("http://localhost").unwrap()), + } + } +} diff --git a/pmoupnp/src/variable_types/display_type.rs b/pmoupnp/src/variable_types/display_type.rs new file mode 100644 index 00000000..7794fd6d --- /dev/null +++ b/pmoupnp/src/variable_types/display_type.rs @@ -0,0 +1,34 @@ +use std::fmt; + +use crate::variable_types::StateVarType; + +impl fmt::Display for StateVarType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let s = match self { + StateVarType::UI1 => "ui1", + StateVarType::UI2 => "ui2", + StateVarType::UI4 => "ui4", + StateVarType::I1 => "i1", + StateVarType::I2 => "i2", + StateVarType::I4 => "i4", + StateVarType::Int => "int", + StateVarType::R4 => "r4", + StateVarType::R8 => "r8", + StateVarType::Number => "number", + StateVarType::Fixed14_4 => "fixed.14.4", + StateVarType::Char => "char", + StateVarType::String => "string", + StateVarType::Boolean => "boolean", + StateVarType::BinBase64 => "bin.base64", + StateVarType::BinHex => "bin.hex", + StateVarType::Date => "date", + StateVarType::DateTime => "dateTime", + StateVarType::DateTimeTZ => "dateTime.tz", + StateVarType::Time => "time", + StateVarType::TimeTZ => "time.tz", + StateVarType::UUID => "uuid", + StateVarType::URI => "uri", + }; + write!(f, "{}", s) + } +} diff --git a/pmoupnp/src/variable_types/display_value.rs b/pmoupnp/src/variable_types/display_value.rs new file mode 100644 index 00000000..9cc18951 --- /dev/null +++ b/pmoupnp/src/variable_types/display_value.rs @@ -0,0 +1,44 @@ +use base64::Engine; +use base64::engine::general_purpose; +use std::fmt; + +use crate::variable_types::StateValue; + +impl fmt::Display for StateValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + // Numériques + StateValue::UI1(v) => write!(f, "{}", v), + StateValue::UI2(v) => write!(f, "{}", v), + StateValue::UI4(v) => write!(f, "{}", v), + StateValue::I1(v) => write!(f, "{}", v), + StateValue::I2(v) => write!(f, "{}", v), + StateValue::I4(v) => write!(f, "{}", v), + StateValue::Int(v) => write!(f, "{}", v), + StateValue::R4(v) => write!(f, "{}", v), + StateValue::R8(v) => write!(f, "{}", v), + StateValue::Number(v) => write!(f, "{}", v), + StateValue::Fixed14_4(v) => write!(f, "{}", v), + + // Types déjà Display + StateValue::Char(v) => write!(f, "{}", v), + StateValue::String(v) => write!(f, "{}", v), + StateValue::UUID(v) => write!(f, "{}", v), + StateValue::URI(v) => write!(f, "{}", v), + + // Booléen : 1 ou 0 + StateValue::Boolean(v) => write!(f, "{}", if *v { "1" } else { "0" }), + + // Encodages binaires + StateValue::BinBase64(v) => write!(f, "{}", general_purpose::URL_SAFE.encode(v)), + StateValue::BinHex(v) => write!(f, "{}", hex::encode(v)), + + // Dates et temps + StateValue::Date(v) => write!(f, "{}", v.format("%Y-%m-%d")), + StateValue::DateTime(v) => write!(f, "{}", v.format("%Y-%m-%dT%H:%M:%S")), + StateValue::DateTimeTZ(v) => write!(f, "{}", v.to_rfc3339()), + StateValue::Time(v) => write!(f, "{}", v.format("%H:%M:%S")), + StateValue::TimeTZ(v) => write!(f, "{}", v.format("%H:%M:%S%z")), + } + } +} diff --git a/pmoupnp/src/variable_types/errors.rs b/pmoupnp/src/variable_types/errors.rs new file mode 100644 index 00000000..04fc26e5 --- /dev/null +++ b/pmoupnp/src/variable_types/errors.rs @@ -0,0 +1,28 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum StateValueError { + #[error("Conversion error: {0}")] + ConversionError(String), + + #[error("Validation error: {0}")] + ValidationError(String), + + #[error("Range error: {0}")] + RangeError(String), + + #[error("Type error: {0}")] + TypeError(String), + + #[error("Parse error: {0}")] + ParseError(String), + + #[error("Event condition error: {0}")] + EventConditionError(String), + + #[error("Arithmetic error: {0}")] + ArithmeticError(String), + + #[error("Unknown error: {0}")] + Unknown(String), +} diff --git a/pmoupnp/src/variable_types/fromstr.rs b/pmoupnp/src/variable_types/fromstr.rs new file mode 100644 index 00000000..ecb1ee5a --- /dev/null +++ b/pmoupnp/src/variable_types/fromstr.rs @@ -0,0 +1,35 @@ +use crate::variable_types::StateVarType; +use std::str::FromStr; + +impl FromStr for StateVarType { + type Err = String; // Type d'erreur personnalisé + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "ui1" => Ok(StateVarType::UI1), + "ui2" => Ok(StateVarType::UI2), + "ui4" => Ok(StateVarType::UI4), + "i1" => Ok(StateVarType::I1), + "i2" => Ok(StateVarType::I2), + "i4" => Ok(StateVarType::I4), + "int" => Ok(StateVarType::Int), + "r4" => Ok(StateVarType::R4), + "r8" => Ok(StateVarType::R8), + "number" => Ok(StateVarType::Number), + "fixed.14.4" => Ok(StateVarType::Fixed14_4), + "char" => Ok(StateVarType::Char), + "string" => Ok(StateVarType::String), + "boolean" => Ok(StateVarType::Boolean), + "bin.base64" => Ok(StateVarType::BinBase64), + "bin.hex" => Ok(StateVarType::BinHex), + "date" => Ok(StateVarType::Date), + "datetime" => Ok(StateVarType::DateTime), + "datetime.tz" => Ok(StateVarType::DateTimeTZ), + "time" => Ok(StateVarType::Time), + "time.tz" => Ok(StateVarType::TimeTZ), + "uuid" => Ok(StateVarType::UUID), + "uri" => Ok(StateVarType::URI), + _ => Err(format!("Type inconnu: {}", s)), + } + } +} diff --git a/pmoupnp/src/variable_types/mod.rs b/pmoupnp/src/variable_types/mod.rs new file mode 100644 index 00000000..ee60afa4 --- /dev/null +++ b/pmoupnp/src/variable_types/mod.rs @@ -0,0 +1,98 @@ +mod cast; +mod default_value; +mod display_type; +mod display_value; +mod errors; +mod fromstr; +mod type_methods; +mod type_trait; +mod value_methods; +mod value_trait; + +mod values_from_type; + +mod values_from_i16; +mod values_from_i32; +mod values_from_i64; +mod values_from_i8; +mod values_from_u16; +mod values_from_u32; +mod values_from_u8; + +mod values_from_f32; +mod values_from_f64; + +mod values_from_datetime; +mod values_from_naivedate; +mod values_from_naivedatetime; +mod values_from_naivetime; +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}; +use url::Url; +use uuid::Uuid; + +pub use errors::StateValueError; +pub use type_trait::UpnpVarType; + +pub use crate::variable_types::value_trait::UpnpValue; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum StateVarType { + UI1, // Unsigned 8-bit integer + UI2, // Unsigned 16-bit integer + UI4, // Unsigned 32-bit integer + I1, // Signed 8-bit integer + I2, // Signed 16-bit integer + I4, // Signed 32-bit integer + Int, // Synonymous with i4 + R4, // 32-bit floating point + R8, // 64-bit floating point + Number, // Synonymous with r8 + Fixed14_4, // Fixed-point decimal + Char, // Single Unicode character + String, // Character string + Boolean, // Boolean value + BinBase64, // Base64-encoded binary + BinHex, // Hex-encoded binary + Date, // Date (YYYY-MM-DD) + DateTime, // DateTime without timezone + DateTimeTZ, // DateTime with timezone + Time, // Time without timezone + TimeTZ, // Time with timezone + UUID, // Universally unique identifier + URI, // Uniform Resource Identifier +} + +#[derive(Clone, Debug)] +pub enum StateValue { + UI1(u8), + UI2(u16), + UI4(u32), + I1(i8), + I2(i16), + I4(i32), + Int(i32), + R4(f32), + R8(f64), + Number(f64), + Fixed14_4(f64), + Char(char), + String(String), + Boolean(bool), + BinBase64(String), + BinHex(String), + Date(NaiveDate), + DateTime(NaiveDateTime), + DateTimeTZ(DateTime), + Time(NaiveTime), + TimeTZ(DateTime), + UUID(Uuid), + URI(Url), +} diff --git a/pmoupnp/src/variable_types/type_methods.rs b/pmoupnp/src/variable_types/type_methods.rs new file mode 100644 index 00000000..2b819a4c --- /dev/null +++ b/pmoupnp/src/variable_types/type_methods.rs @@ -0,0 +1,109 @@ +use crate::variable_types::{StateVarType, type_trait::UpnpVarType}; + +impl UpnpVarType for StateVarType { + fn as_state_var_type(&self) -> StateVarType { + *self + } + + fn bit_size(&self) -> Option { + match self { + StateVarType::UI1 | StateVarType::I1 => Some(8), + StateVarType::UI2 | StateVarType::I2 => Some(16), + StateVarType::UI4 | StateVarType::I4 | StateVarType::Int | StateVarType::R4 => Some(32), + StateVarType::R8 | StateVarType::Number | StateVarType::Fixed14_4 => Some(64), + _ => None, + } + } + + fn is_numeric(&self) -> bool { + matches!( + self, + StateVarType::UI1 + | StateVarType::UI2 + | StateVarType::UI4 + | StateVarType::I1 + | StateVarType::I2 + | StateVarType::I4 + | StateVarType::Int + | StateVarType::R4 + | StateVarType::R8 + | StateVarType::Number + | StateVarType::Fixed14_4 + ) + } + + fn is_integer(&self) -> bool { + matches!( + self, + StateVarType::UI1 + | StateVarType::UI2 + | StateVarType::UI4 + | StateVarType::I1 + | StateVarType::I2 + | StateVarType::I4 + | StateVarType::Int + ) + } + + fn is_signed_int(&self) -> bool { + matches!( + self, + StateVarType::I1 | StateVarType::I2 | StateVarType::I4 | StateVarType::Int + ) + } + + fn is_unsigned_int(&self) -> bool { + matches!( + self, + StateVarType::UI1 | StateVarType::UI2 | StateVarType::UI4 + ) + } + + fn is_float(&self) -> bool { + matches!( + self, + StateVarType::R4 | StateVarType::R8 | StateVarType::Number | StateVarType::Fixed14_4 + ) + } + + fn is_bool(&self) -> bool { + matches!(self, StateVarType::Boolean) + } + + fn is_string(&self) -> bool { + matches!( + self, + StateVarType::String + | StateVarType::Char + | StateVarType::BinHex + | StateVarType::BinBase64 + ) + } + + fn is_time(&self) -> bool { + matches!( + self, + StateVarType::Date + | StateVarType::DateTime + | StateVarType::DateTimeTZ + | StateVarType::Time + | StateVarType::TimeTZ + ) + } + + fn is_uuid(&self) -> bool { + matches!(self, StateVarType::UUID) + } + + fn is_uri(&self) -> bool { + matches!(self, StateVarType::URI) + } + + fn is_binary(&self) -> bool { + matches!(self, StateVarType::BinBase64 | StateVarType::BinHex) + } + + fn is_comparable(&self) -> bool { + !self.is_binary() + } +} diff --git a/pmoupnp/src/variable_types/type_trait.rs b/pmoupnp/src/variable_types/type_trait.rs new file mode 100644 index 00000000..d72d2ea6 --- /dev/null +++ b/pmoupnp/src/variable_types/type_trait.rs @@ -0,0 +1,57 @@ +use crate::variable_types::StateVarType; + +pub trait UpnpVarType { + fn as_state_var_type(&self) -> StateVarType; + + fn bit_size(&self) -> Option { + self.as_state_var_type().bit_size() + } + + fn is_numeric(&self) -> bool { + self.as_state_var_type().is_numeric() + } + + fn is_integer(&self) -> bool { + self.as_state_var_type().is_integer() + } + + fn is_signed_int(&self) -> bool { + self.as_state_var_type().is_signed_int() + } + + fn is_unsigned_int(&self) -> bool { + self.as_state_var_type().is_unsigned_int() + } + + fn is_float(&self) -> bool { + self.as_state_var_type().is_float() + } + + fn is_bool(&self) -> bool { + self.as_state_var_type().is_bool() + } + + fn is_string(&self) -> bool { + self.as_state_var_type().is_string() + } + + fn is_time(&self) -> bool { + self.as_state_var_type().is_time() + } + + fn is_uuid(&self) -> bool { + self.as_state_var_type().is_uuid() + } + + fn is_uri(&self) -> bool { + self.as_state_var_type().is_uri() + } + + fn is_binary(&self) -> bool { + self.as_state_var_type().is_binary() + } + + fn is_comparable(&self) -> bool { + self.as_state_var_type().is_comparable() + } +} diff --git a/pmoupnp/src/variable_types/value_methods.rs b/pmoupnp/src/variable_types/value_methods.rs new file mode 100644 index 00000000..d7ef8f67 --- /dev/null +++ b/pmoupnp/src/variable_types/value_methods.rs @@ -0,0 +1,88 @@ +use std::cmp::Ordering; + +use crate::variable_types::{StateValue, StateVarType, type_trait::UpnpVarType}; + +impl UpnpVarType for StateValue { + fn as_state_var_type(&self) -> StateVarType { + StateVarType::from(self) // utilise ton From<&StateValue> existant + } +} + +impl PartialEq for StateValue { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (a, b) if a.is_integer() && b.is_integer() => { + if let (Ok(ia), Ok(ib)) = (i64::try_from(a), i64::try_from(b)) { + return ia == ib; + }; + return false; + } + (a, b) if a.is_float() && b.is_float() => { + if let (Ok(a), Ok(b)) = (f64::try_from(self), f64::try_from(other)) { + return a == b; // NaN respecte la sémantique IEEE (NaN != NaN) + } + return false; + } + (a, b) if a.is_string() && b.is_string() => { + let (a, b) = (self.to_string(), other.to_string()); + return a == b; // NaN respecte la sémantique IEEE (NaN != NaN) + } + (StateValue::Date(a), StateValue::Date(b)) => { + return a == b; + } + (StateValue::Time(a), StateValue::Time(b)) => { + return a == b; + } + (StateValue::DateTime(a), StateValue::DateTime(b)) => { + return a == b; + } + (StateValue::DateTimeTZ(a), StateValue::DateTimeTZ(b)) => { + return a == b; + } + (StateValue::TimeTZ(a), StateValue::TimeTZ(b)) => { + return a == b; + } + + (_, _) => return false, + } + } +} + +impl PartialOrd for StateValue { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (a, b) if a.is_integer() && b.is_integer() => { + if let (Ok(ia), Ok(ib)) = (i64::try_from(a), i64::try_from(b)) { + return Some(ia.cmp(&ib)); + }; + return None; + } + (a, b) if a.is_float() && b.is_float() => { + if let (Ok(a), Ok(b)) = (f64::try_from(self), f64::try_from(other)) { + return a.partial_cmp(&b); // NaN respecte la sémantique IEEE (NaN != NaN) + } + return None; + } + (a, b) if a.is_string() && b.is_string() => { + let (a, b) = (self.to_string(), other.to_string()); + return Some(a.cmp(&b)); // NaN respecte la sémantique IEEE (NaN != NaN) + } + (StateValue::Date(a), StateValue::Date(b)) => { + return Some(a.cmp(&b)); + } + (StateValue::Time(a), StateValue::Time(b)) => { + return Some(a.cmp(&b)); + } + (StateValue::DateTime(a), StateValue::DateTime(b)) => { + return Some(a.cmp(&b)); + } + (StateValue::DateTimeTZ(a), StateValue::DateTimeTZ(b)) => { + return Some(a.cmp(&b)); + } + (StateValue::TimeTZ(a), StateValue::TimeTZ(b)) => { + return Some(a.cmp(&b)); + } + (_, _) => return None, + } + } +} diff --git a/pmoupnp/src/variable_types/value_trait.rs b/pmoupnp/src/variable_types/value_trait.rs new file mode 100644 index 00000000..7dccd192 --- /dev/null +++ b/pmoupnp/src/variable_types/value_trait.rs @@ -0,0 +1 @@ +pub trait UpnpValue: Clone {} diff --git a/pmoupnp/src/variable_types/values_from_datetime.rs b/pmoupnp/src/variable_types/values_from_datetime.rs new file mode 100644 index 00000000..ee511664 --- /dev/null +++ b/pmoupnp/src/variable_types/values_from_datetime.rs @@ -0,0 +1,37 @@ +use crate::variable_types::{StateValue, StateValueError}; +use chrono::{DateTime, FixedOffset}; +use std::convert::TryFrom; + +impl TryFrom<&StateValue> for DateTime { + type Error = StateValueError; + + fn try_from(value: &StateValue) -> Result { + match value { + StateValue::DateTimeTZ(v) => Ok(v.clone()), + StateValue::TimeTZ(v) => Ok(v.clone()), + StateValue::String(v) => DateTime::parse_from_rfc3339(v).map_err(|e| { + StateValueError::ParseError(format!( + "Cannot parse DateTimeTZ from string '{}': {}", + v, e + )) + }), + _ => Err(StateValueError::TypeError( + "Cannot cast to DateTime".into(), + )), + } + } +} + +impl TryFrom for DateTime { + type Error = StateValueError; + + fn try_from(value: StateValue) -> Result { + DateTime::::try_from(&value) + } +} + +impl From> for StateValue { + fn from(value: DateTime) -> Self { + StateValue::DateTimeTZ(value) + } +} diff --git a/pmoupnp/src/variable_types/values_from_f32.rs b/pmoupnp/src/variable_types/values_from_f32.rs new file mode 100644 index 00000000..dae0f0cc --- /dev/null +++ b/pmoupnp/src/variable_types/values_from_f32.rs @@ -0,0 +1,76 @@ +use std::convert::TryFrom; + +use crate::variable_types::{StateValue, StateValueError}; + +impl TryFrom<&StateValue> for f32 { + type Error = StateValueError; + + fn try_from(value: &StateValue) -> Result { + const MAX_EXACT: i32 = 1 << f32::MANTISSA_DIGITS; + match value { + // --- Signed integers --- + StateValue::I1(v) => Ok(*v as f32), + StateValue::I2(v) => Ok(*v as f32), + StateValue::I4(v) if *v > -MAX_EXACT && *v < MAX_EXACT => Ok(*v as f32), + StateValue::Int(v) if *v >= -MAX_EXACT && *v <= MAX_EXACT as i32 => Ok(*v as f32), + + // --- Unsigned integers --- + StateValue::UI1(v) => Ok(*v as f32), + StateValue::UI2(v) => Ok(*v as f32), + StateValue::UI4(v) if *v <= MAX_EXACT as u32 => Ok(*v as f32), + StateValue::UI4(_) => Err(StateValueError::TypeError( + "Cannot cast UI4 to f32: out of range".into(), + )), + + // --- Floats --- + StateValue::R4(v) => Ok(*v), // déjà un f32 + StateValue::R8(v) + if !v.is_finite() || (*v <= f32::MAX as f64 && *v >= f32::MIN as f64) => + { + Ok(*v as f32) + } + StateValue::R8(_) => Err(StateValueError::TypeError( + "Cannot cast R8 to f32: out of range".into(), + )), + StateValue::Number(v) + if !v.is_finite() || (*v <= f32::MAX as f64 && *v >= f32::MIN as f64) => + { + Ok(*v as f32) + } + StateValue::Number(_) => Err(StateValueError::TypeError( + "Cannot cast Number to f32: out of range".into(), + )), + StateValue::Fixed14_4(v) + if !v.is_finite() || (*v <= f32::MAX as f64 && *v >= f32::MIN as f64) => + { + Ok(*v as f32) + } + StateValue::Fixed14_4(_) => Err(StateValueError::TypeError( + "Cannot cast Fixed14_4 to f32: out of range".into(), + )), + + StateValue::Boolean(v) => Ok((*v as i32) as Self), + + StateValue::String(s) => s + .parse::() + .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as f32", s))), + + // --- Par défaut : erreur --- + _ => Err(StateValueError::TypeError("Cannot cast to f32".into())), + } + } +} + +impl TryFrom for f32 { + type Error = StateValueError; + + fn try_from(value: StateValue) -> Result { + f32::try_from(&value) + } +} + +impl From for StateValue { + fn from(value: f32) -> Self { + StateValue::R4(value) + } +} diff --git a/pmoupnp/src/variable_types/values_from_f64.rs b/pmoupnp/src/variable_types/values_from_f64.rs new file mode 100644 index 00000000..cfb18419 --- /dev/null +++ b/pmoupnp/src/variable_types/values_from_f64.rs @@ -0,0 +1,51 @@ +use std::convert::TryFrom; + +use crate::variable_types::{StateValue, StateValueError}; + +impl TryFrom<&StateValue> for f64 { + type Error = StateValueError; + + fn try_from(value: &StateValue) -> Result { + match value { + // --- Signed integers --- + StateValue::I1(v) => Ok(*v as f64), + StateValue::I2(v) => Ok(*v as f64), + StateValue::I4(v) => Ok(*v as f64), + StateValue::Int(v) => Ok(*v as f64), + + // --- Unsigned integers --- + StateValue::UI1(v) => Ok(*v as f64), + StateValue::UI2(v) => Ok(*v as f64), + StateValue::UI4(v) => Ok(*v as f64), + + // --- Floats --- + StateValue::R4(v) => Ok(*v as f64), + StateValue::R8(v) => Ok(*v), + StateValue::Number(v) => Ok(*v), + StateValue::Fixed14_4(v) => Ok(*v), + + StateValue::Boolean(v) => Ok((*v as i32) as Self), + + StateValue::String(s) => s + .parse::() + .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as f64", s))), + + // --- Par défaut : erreur --- + _ => Err(StateValueError::TypeError("Cannot cast to f64".into())), + } + } +} + +impl TryFrom for f64 { + type Error = StateValueError; + + fn try_from(value: StateValue) -> Result { + f64::try_from(&value) + } +} + +impl From for StateValue { + fn from(value: f64) -> Self { + StateValue::R8(value) + } +} diff --git a/pmoupnp/src/variable_types/values_from_i16.rs b/pmoupnp/src/variable_types/values_from_i16.rs new file mode 100644 index 00000000..a99f4c74 --- /dev/null +++ b/pmoupnp/src/variable_types/values_from_i16.rs @@ -0,0 +1,43 @@ +use std::convert::TryFrom; + +use crate::variable_types::{StateValue, StateValueError}; + +// Implémentations TryFrom pour types numériques + +impl TryFrom<&StateValue> for i16 { + type Error = StateValueError; + + fn try_from(value: &StateValue) -> Result { + match value { + StateValue::I1(v) => Ok(*v as i16), + StateValue::I2(v) => Ok(*v), + StateValue::I4(v) if *v <= i16::MAX as i32 && *v >= i16::MIN as i32 => Ok(*v as i16), + StateValue::Int(v) if *v <= i16::MAX as i32 && *v >= i16::MIN as i32 => Ok(*v as i16), + + StateValue::UI1(v) => Ok(*v as i16), + StateValue::UI2(v) if *v <= i16::MAX as u16 => Ok(*v as i16), + StateValue::UI4(v) if *v <= i16::MAX as u32 => Ok(*v as i16), + StateValue::Boolean(v) => Ok(*v as Self), + + StateValue::String(s) => s + .parse::() + .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as i16", s))), + + _ => Err(StateValueError::TypeError("Cannot cast to i32".into())), + } + } +} + +impl TryFrom for i16 { + type Error = StateValueError; + + fn try_from(value: StateValue) -> Result { + i16::try_from(&value) + } +} + +impl From for StateValue { + fn from(value: i16) -> Self { + StateValue::I2(value) + } +} diff --git a/pmoupnp/src/variable_types/values_from_i32.rs b/pmoupnp/src/variable_types/values_from_i32.rs new file mode 100644 index 00000000..b561f0ae --- /dev/null +++ b/pmoupnp/src/variable_types/values_from_i32.rs @@ -0,0 +1,43 @@ +use std::convert::TryFrom; + +use crate::variable_types::{StateValue, StateValueError}; + +impl TryFrom<&StateValue> for i32 { + type Error = StateValueError; + + fn try_from(value: &StateValue) -> Result { + match value { + // signés + StateValue::I1(v) => Ok(*v as i32), + StateValue::I2(v) => Ok(*v as i32), + StateValue::I4(v) => Ok(*v), + StateValue::Int(v) => Ok(*v), + + // non signés + StateValue::UI1(v) => Ok(*v as i32), + StateValue::UI2(v) => Ok(*v as i32), + StateValue::UI4(v) if *v <= i32::MAX as u32 => Ok(*v as i32), + StateValue::Boolean(v) => Ok(*v as Self), + + StateValue::String(s) => s + .parse::() + .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as i32", s))), + + _ => Err(StateValueError::TypeError("Cannot cast to i32".into())), + } + } +} + +impl TryFrom for i32 { + type Error = StateValueError; + + fn try_from(value: StateValue) -> Result { + i32::try_from(&value) + } +} + +impl From for StateValue { + fn from(value: i32) -> Self { + StateValue::I4(value) + } +} diff --git a/pmoupnp/src/variable_types/values_from_i64.rs b/pmoupnp/src/variable_types/values_from_i64.rs new file mode 100644 index 00000000..8550196d --- /dev/null +++ b/pmoupnp/src/variable_types/values_from_i64.rs @@ -0,0 +1,45 @@ +use crate::variable_types::{StateValue, StateValueError}; +use std::convert::TryFrom; + +impl TryFrom<&StateValue> for i64 { + type Error = StateValueError; + + fn try_from(value: &StateValue) -> Result { + match value { + // signés + StateValue::I1(v) => Ok(*v as i64), + StateValue::I2(v) => Ok(*v as i64), + StateValue::I4(v) => Ok(*v as i64), + StateValue::Int(v) => Ok(*v as i64), + + // non signés + StateValue::UI1(v) => Ok(*v as i64), + StateValue::UI2(v) => Ok(*v as i64), + StateValue::UI4(v) => Ok(*v as i64), // toujours dans l'intervalle d'un i64 + + // booléen + StateValue::Boolean(v) => Ok(*v as i64), + + // chaîne → i64 + StateValue::String(s) => s + .parse::() + .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as i64", s))), + + _ => Err(StateValueError::TypeError("Cannot cast to i64".into())), + } + } +} + +impl TryFrom for i64 { + type Error = StateValueError; + + fn try_from(value: StateValue) -> Result { + i64::try_from(&value) + } +} + +impl From for StateValue { + fn from(value: i64) -> Self { + StateValue::Int(value as i32) // ⚠️ choix à discuter : Int est i32, pas i64 + } +} diff --git a/pmoupnp/src/variable_types/values_from_i8.rs b/pmoupnp/src/variable_types/values_from_i8.rs new file mode 100644 index 00000000..ce15cab0 --- /dev/null +++ b/pmoupnp/src/variable_types/values_from_i8.rs @@ -0,0 +1,43 @@ +use std::convert::TryFrom; + +use crate::variable_types::{StateValue, StateValueError}; + +// Implémentations TryFrom pour types numériques + +impl TryFrom<&StateValue> for i8 { + type Error = StateValueError; + + fn try_from(value: &StateValue) -> Result { + match value { + StateValue::I1(v) if *v >= 0 => Ok(*v as i8), + StateValue::I2(v) if *v <= i8::MAX as i16 && *v >= i8::MIN as i16 => Ok(*v as i8), + StateValue::I4(v) if *v <= i8::MAX as i32 && *v >= i8::MIN as i32 => Ok(*v as i8), + StateValue::Int(v) if *v <= i8::MAX as i32 && *v >= i8::MIN as i32 => Ok(*v as i8), + + StateValue::UI1(v) if *v <= i8::MAX as u8 => Ok(*v as i8), + StateValue::UI2(v) if *v <= i8::MAX as u16 => Ok(*v as i8), + StateValue::UI4(v) if *v <= i8::MAX as u32 => Ok(*v as i8), + StateValue::Boolean(v) => Ok(*v as Self), + + StateValue::String(s) => s + .parse::() + .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as i8", s))), + + _ => Err(StateValueError::TypeError("Cannot cast to i8".into())), + } + } +} + +impl TryFrom for i8 { + type Error = StateValueError; + + fn try_from(value: StateValue) -> Result { + i8::try_from(&value) + } +} + +impl From for StateValue { + fn from(value: i8) -> Self { + StateValue::I1(value) + } +} diff --git a/pmoupnp/src/variable_types/values_from_naivedate.rs b/pmoupnp/src/variable_types/values_from_naivedate.rs new file mode 100644 index 00000000..db711533 --- /dev/null +++ b/pmoupnp/src/variable_types/values_from_naivedate.rs @@ -0,0 +1,33 @@ +use crate::variable_types::{StateValue, StateValueError}; +use chrono::NaiveDate; +use std::convert::TryFrom; + +impl TryFrom<&StateValue> for NaiveDate { + type Error = StateValueError; + + fn try_from(value: &StateValue) -> Result { + match value { + StateValue::Date(v) => Ok(v.clone()), + StateValue::String(v) => NaiveDate::parse_from_str(v, "%Y-%m-%d").map_err(|e| { + StateValueError::ParseError(format!("Cannot parse Date from string '{}': {}", v, e)) + }), + _ => Err(StateValueError::TypeError( + "Cannot cast to NaiveDate".into(), + )), + } + } +} + +impl TryFrom for NaiveDate { + type Error = StateValueError; + + fn try_from(value: StateValue) -> Result { + NaiveDate::try_from(&value) + } +} + +impl From for StateValue { + fn from(value: NaiveDate) -> Self { + StateValue::Date(value) + } +} diff --git a/pmoupnp/src/variable_types/values_from_naivedatetime.rs b/pmoupnp/src/variable_types/values_from_naivedatetime.rs new file mode 100644 index 00000000..8395af91 --- /dev/null +++ b/pmoupnp/src/variable_types/values_from_naivedatetime.rs @@ -0,0 +1,37 @@ +use crate::variable_types::{StateValue, StateValueError}; +use chrono::NaiveDateTime; +use std::convert::TryFrom; + +impl TryFrom<&StateValue> for NaiveDateTime { + type Error = StateValueError; + + fn try_from(value: &StateValue) -> Result { + match value { + StateValue::DateTime(v) => Ok(v.clone()), + StateValue::String(v) => NaiveDateTime::parse_from_str(&v, "%Y-%m-%dT%H:%M:%S") + .map_err(|e| { + StateValueError::ParseError(format!( + "Cannot parse DateTime from string '{}': {}", + v, e + )) + }), + _ => Err(StateValueError::TypeError( + "Cannot cast to NaiveDateTime".into(), + )), + } + } +} + +impl TryFrom for NaiveDateTime { + type Error = StateValueError; + + fn try_from(value: StateValue) -> Result { + NaiveDateTime::try_from(&value) + } +} + +impl From for StateValue { + fn from(value: NaiveDateTime) -> Self { + StateValue::DateTime(value) + } +} diff --git a/pmoupnp/src/variable_types/values_from_naivetime.rs b/pmoupnp/src/variable_types/values_from_naivetime.rs new file mode 100644 index 00000000..1127898a --- /dev/null +++ b/pmoupnp/src/variable_types/values_from_naivetime.rs @@ -0,0 +1,33 @@ +use crate::variable_types::{StateValue, StateValueError}; +use chrono::NaiveTime; +use std::convert::TryFrom; + +impl TryFrom<&StateValue> for NaiveTime { + type Error = StateValueError; + + fn try_from(value: &StateValue) -> Result { + match value { + StateValue::Time(v) => Ok(v.clone()), + StateValue::String(v) => NaiveTime::parse_from_str(&v, "%H:%M:%S").map_err(|e| { + StateValueError::ParseError(format!("Cannot parse Time from string '{}': {}", v, e)) + }), + _ => Err(StateValueError::TypeError( + "Cannot cast to NaiveTime".into(), + )), + } + } +} + +impl TryFrom for NaiveTime { + type Error = StateValueError; + + fn try_from(value: StateValue) -> Result { + NaiveTime::try_from(&value) + } +} + +impl From for StateValue { + fn from(value: NaiveTime) -> Self { + StateValue::Time(value) + } +} 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..1054f650 --- /dev/null +++ b/pmoupnp/src/variable_types/values_from_str.rs @@ -0,0 +1,20 @@ +use std::convert::TryFrom; + +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/src/variable_types/values_from_type.rs b/pmoupnp/src/variable_types/values_from_type.rs new file mode 100644 index 00000000..f28a3789 --- /dev/null +++ b/pmoupnp/src/variable_types/values_from_type.rs @@ -0,0 +1,37 @@ +use crate::variable_types::{StateValue, StateVarType}; + +impl From<&StateValue> for StateVarType { + fn from(value: &StateValue) -> Self { + match value { + StateValue::UI1(_) => StateVarType::UI1, + StateValue::UI2(_) => StateVarType::UI2, + StateValue::UI4(_) => StateVarType::UI4, + StateValue::I1(_) => StateVarType::I1, + StateValue::I2(_) => StateVarType::I2, + StateValue::I4(_) => StateVarType::I4, + StateValue::Int(_) => StateVarType::Int, + StateValue::R4(_) => StateVarType::R4, + StateValue::R8(_) => StateVarType::R8, + StateValue::Number(_) => StateVarType::Number, + StateValue::Fixed14_4(_) => StateVarType::Fixed14_4, + StateValue::Char(_) => StateVarType::Char, + StateValue::String(_) => StateVarType::String, + StateValue::BinBase64(_) => StateVarType::BinBase64, + StateValue::BinHex(_) => StateVarType::BinHex, + StateValue::URI(_) => StateVarType::URI, + StateValue::UUID(_) => StateVarType::UUID, + StateValue::Date(_) => StateVarType::Date, + StateValue::DateTime(_) => StateVarType::DateTime, + StateValue::DateTimeTZ(_) => StateVarType::DateTimeTZ, + StateValue::Time(_) => StateVarType::Time, + StateValue::TimeTZ(_) => StateVarType::TimeTZ, + StateValue::Boolean(_) => StateVarType::Boolean, + } + } +} + +impl From for StateVarType { + fn from(value: StateValue) -> Self { + StateVarType::from(&value) + } +} diff --git a/pmoupnp/src/variable_types/values_from_u16.rs b/pmoupnp/src/variable_types/values_from_u16.rs new file mode 100644 index 00000000..1695dca3 --- /dev/null +++ b/pmoupnp/src/variable_types/values_from_u16.rs @@ -0,0 +1,42 @@ +use std::convert::TryFrom; + +use crate::variable_types::{StateValue, StateValueError}; + +// Implémentations TryFrom pour types numériques + +impl TryFrom<&StateValue> for u16 { + type Error = StateValueError; + + fn try_from(value: &StateValue) -> Result { + match value { + StateValue::UI1(v) => Ok(*v as Self), + StateValue::UI2(v) => Ok(*v), + StateValue::UI4(v) if *v <= i16::MAX as u32 => Ok(*v as Self), + StateValue::I1(v) if *v >= 0 => Ok(*v as Self), + StateValue::I2(v) if *v >= 0 => Ok(*v as Self), + StateValue::I4(v) if *v >= 0 && *v <= u16::MAX as i32 => Ok(*v as Self), + StateValue::Int(v) if *v >= 0 && *v <= u16::MAX as i32 => Ok(*v as Self), + StateValue::Boolean(v) => Ok(*v as Self), + + StateValue::String(s) => s + .parse::() + .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as u16", s))), + + _ => Err(StateValueError::TypeError("Cannot cast to u16".into())), + } + } +} + +impl TryFrom for u16 { + type Error = StateValueError; + + fn try_from(value: StateValue) -> Result { + u16::try_from(&value) + } +} + +impl From for StateValue { + fn from(value: u16) -> Self { + StateValue::UI2(value) + } +} diff --git a/pmoupnp/src/variable_types/values_from_u32.rs b/pmoupnp/src/variable_types/values_from_u32.rs new file mode 100644 index 00000000..d28c9bdb --- /dev/null +++ b/pmoupnp/src/variable_types/values_from_u32.rs @@ -0,0 +1,40 @@ +use std::convert::TryFrom; + +use crate::variable_types::{StateValue, StateValueError}; + +impl TryFrom<&StateValue> for u32 { + type Error = StateValueError; + + fn try_from(value: &StateValue) -> Result { + match value { + StateValue::UI1(v) => Ok(*v as Self), + StateValue::UI2(v) => Ok(*v as Self), + StateValue::UI4(v) => Ok(*v), + StateValue::I1(v) if *v >= 0 => Ok(*v as Self), + StateValue::I2(v) if *v >= 0 => Ok(*v as Self), + StateValue::I4(v) if *v >= 0 => Ok(*v as Self), + StateValue::Int(v) if *v >= 0 => Ok(*v as Self), + StateValue::Boolean(v) => Ok(*v as Self), + + StateValue::String(s) => s + .parse::() + .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as u32", s))), + + _ => Err(StateValueError::TypeError("Cannot cast to u32".into())), + } + } +} + +impl TryFrom for u32 { + type Error = StateValueError; + + fn try_from(value: StateValue) -> Result { + u32::try_from(&value) + } +} + +impl From for StateValue { + fn from(value: u32) -> Self { + StateValue::UI4(value) + } +} diff --git a/pmoupnp/src/variable_types/values_from_u8.rs b/pmoupnp/src/variable_types/values_from_u8.rs new file mode 100644 index 00000000..f8ced643 --- /dev/null +++ b/pmoupnp/src/variable_types/values_from_u8.rs @@ -0,0 +1,42 @@ +use std::convert::TryFrom; + +use crate::variable_types::{StateValue, StateValueError}; + +// Implémentations TryFrom pour types numériques + +impl TryFrom<&StateValue> for u8 { + type Error = StateValueError; + + fn try_from(value: &StateValue) -> Result { + match value { + StateValue::UI1(v) => Ok(*v), + StateValue::UI2(v) if *v <= u8::MAX as u16 => Ok(*v as u8), + StateValue::UI4(v) if *v <= i8::MAX as u32 => Ok(*v as u8), + StateValue::I1(v) if *v >= 0 => Ok(*v as u8), + StateValue::I2(v) if *v >= 0 && *v <= u8::MAX as i16 => Ok(*v as u8), + StateValue::I4(v) if *v >= 0 && *v <= u8::MAX as i32 => Ok(*v as u8), + StateValue::Int(v) if *v >= 0 && *v <= u8::MAX as i32 => Ok(*v as u8), + StateValue::Boolean(v) => Ok(*v as Self), + + StateValue::String(s) => s + .parse::() + .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as u8", s))), + + _ => Err(StateValueError::TypeError("Cannot cast to u8".into())), + } + } +} + +impl TryFrom for u8 { + type Error = StateValueError; + + fn try_from(value: StateValue) -> Result { + u8::try_from(&value) + } +} + +impl From for StateValue { + fn from(value: u8) -> Self { + StateValue::UI1(value) + } +} diff --git a/pmoupnp/src/variable_types/values_from_uri.rs b/pmoupnp/src/variable_types/values_from_uri.rs new file mode 100644 index 00000000..cf1a1977 --- /dev/null +++ b/pmoupnp/src/variable_types/values_from_uri.rs @@ -0,0 +1,29 @@ +use std::convert::TryFrom; +use url::Url; + +use crate::variable_types::{StateValue, StateValueError}; + +impl TryFrom for Url { + type Error = StateValueError; + + fn try_from(value: StateValue) -> Result { + match value { + // Si déjà un URI encodé comme StateValue::URI + StateValue::URI(v) => Ok(v), + + // Si c'est une String, on tente un parse + StateValue::String(v) => { + Url::parse(&v).map_err(|_| StateValueError::TypeError("Invalid URI string".into())) + } + + // Autres types : erreur + _ => Err(StateValueError::TypeError("Cannot cast to Url".into())), + } + } +} + +impl From for StateValue { + fn from(value: Url) -> Self { + StateValue::URI(value) + } +} diff --git a/pmoupnp/src/variable_types/values_from_uuid.rs b/pmoupnp/src/variable_types/values_from_uuid.rs new file mode 100644 index 00000000..cd0b1369 --- /dev/null +++ b/pmoupnp/src/variable_types/values_from_uuid.rs @@ -0,0 +1,28 @@ +use std::convert::TryFrom; +use uuid::Uuid; + +use crate::variable_types::{StateValue, StateValueError}; + +impl TryFrom for Uuid { + type Error = StateValueError; + + fn try_from(value: StateValue) -> Result { + match value { + // Si déjà un URI encodé comme StateValue::URI + StateValue::UUID(v) => Ok(v), + + // Si c'est une String, on tente un parse + StateValue::String(v) => Uuid::parse_str(&v) + .map_err(|_| StateValueError::TypeError("Invalid UUID string".into())), + + // Autres types : erreur + _ => Err(StateValueError::TypeError("Cannot cast to Uuid".into())), + } + } +} + +impl From for StateValue { + fn from(value: Uuid) -> Self { + StateValue::UUID(value) + } +} diff --git a/pmoupnp/src/variable_types/values_from_vec_u8.rs b/pmoupnp/src/variable_types/values_from_vec_u8.rs new file mode 100644 index 00000000..3ea60848 --- /dev/null +++ b/pmoupnp/src/variable_types/values_from_vec_u8.rs @@ -0,0 +1,46 @@ +use crate::variable_types::{StateValue, StateValueError}; +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use std::convert::TryFrom; + +impl TryFrom<&StateValue> for Vec { + type Error = StateValueError; + + fn try_from(value: &StateValue) -> Result { + match value { + // Déjà un vecteur binaire + StateValue::BinBase64(v) => STANDARD + .decode(v) + .map_err(|e| StateValueError::ParseError(format!("Base64 decode error: {}", e))), + StateValue::BinHex(v) => hex::decode(v) + .map_err(|e| StateValueError::ParseError(format!("BinHex decode error: {}", e))), + + // Conversion depuis une chaîne encodée + StateValue::String(s) => { + // Essayer Base64 + if let Ok(bytes) = STANDARD.decode(s) { + return Ok(bytes); + } + // Essayer Hex + if let Ok(bytes) = hex::decode(s) { + return Ok(bytes); + } + Err(StateValueError::ParseError(format!( + "Cannot parse string '{}' as binary", + s + ))) + } + + _ => Err(StateValueError::TypeError( + "Cannot cast to binary Vec".into(), + )), + } + } +} + +impl TryFrom for Vec { + type Error = StateValueError; + + fn try_from(value: StateValue) -> Result { + Vec::::try_from(&value) + } +} diff --git a/pmoupnp/webapp/.gitignore b/pmoupnp/webapp/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/pmoupnp/webapp/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/pmoupnp/webapp/.vscode/extensions.json b/pmoupnp/webapp/.vscode/extensions.json new file mode 100644 index 00000000..a7cea0b0 --- /dev/null +++ b/pmoupnp/webapp/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/pmoupnp/webapp/README.md b/pmoupnp/webapp/README.md new file mode 100644 index 00000000..33895ab2 --- /dev/null +++ b/pmoupnp/webapp/README.md @@ -0,0 +1,5 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/pmoupnp/webapp/package-lock.json b/pmoupnp/webapp/package-lock.json new file mode 100644 index 00000000..e8814061 --- /dev/null +++ b/pmoupnp/webapp/package-lock.json @@ -0,0 +1,1486 @@ +{ + "name": "webapp", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "webapp", + "version": "0.0.0", + "dependencies": { + "dompurify": "^3.2.7", + "marked": "^16.3.0", + "vue": "^3.5.21", + "vue-router": "^4.5.1" + }, + "devDependencies": { + "@types/dompurify": "^3.0.5", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/tsconfig": "^0.8.1", + "typescript": "~5.8.3", + "vite": "^7.1.7", + "vue-tsc": "^3.0.7" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz", + "integrity": "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz", + "integrity": "sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.3.tgz", + "integrity": "sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.3.tgz", + "integrity": "sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.3.tgz", + "integrity": "sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.3.tgz", + "integrity": "sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.3.tgz", + "integrity": "sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.3.tgz", + "integrity": "sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.3.tgz", + "integrity": "sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.3.tgz", + "integrity": "sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.3.tgz", + "integrity": "sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.3.tgz", + "integrity": "sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.3.tgz", + "integrity": "sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.3.tgz", + "integrity": "sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.3.tgz", + "integrity": "sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.3.tgz", + "integrity": "sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.3.tgz", + "integrity": "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.3.tgz", + "integrity": "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.3.tgz", + "integrity": "sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.3.tgz", + "integrity": "sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.3.tgz", + "integrity": "sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz", + "integrity": "sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.3.tgz", + "integrity": "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz", + "integrity": "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.29" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz", + "integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.23" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.23.tgz", + "integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.23.tgz", + "integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.23", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.22.tgz", + "integrity": "sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.4", + "@vue/shared": "3.5.22", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz", + "integrity": "sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.22", + "@vue/shared": "3.5.22" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz", + "integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.4", + "@vue/compiler-core": "3.5.22", + "@vue/compiler-dom": "3.5.22", + "@vue/compiler-ssr": "3.5.22", + "@vue/shared": "3.5.22", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.19", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.22.tgz", + "integrity": "sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.22", + "@vue/shared": "3.5.22" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.0.tgz", + "integrity": "sha512-a7ns+X9vTbdmk7QLrvnZs8s4E1wwtxG/sELzr6F2j4pU+r/OoAv6jJGSz+5tVTU6e4+3rjepGhSP8jDmBBcb3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.23", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.0.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.2" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.22.tgz", + "integrity": "sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.22" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.22.tgz", + "integrity": "sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.22", + "@vue/shared": "3.5.22" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz", + "integrity": "sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.22", + "@vue/runtime-core": "3.5.22", + "@vue/shared": "3.5.22", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.22.tgz", + "integrity": "sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.22", + "@vue/shared": "3.5.22" + }, + "peerDependencies": { + "vue": "3.5.22" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.22.tgz", + "integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.8.1.tgz", + "integrity": "sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "5.x", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/alien-signals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.0.0.tgz", + "integrity": "sha512-JHoRJf18Y6HN4/KZALr3iU+0vW9LKG+8FMThQlbn4+gv8utsLIkwpomjElGPccGeNwh0FI2HN6BLnyFLo6OyLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/marked": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.3.0.tgz", + "integrity": "sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz", + "integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.3", + "@rollup/rollup-android-arm64": "4.52.3", + "@rollup/rollup-darwin-arm64": "4.52.3", + "@rollup/rollup-darwin-x64": "4.52.3", + "@rollup/rollup-freebsd-arm64": "4.52.3", + "@rollup/rollup-freebsd-x64": "4.52.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.3", + "@rollup/rollup-linux-arm-musleabihf": "4.52.3", + "@rollup/rollup-linux-arm64-gnu": "4.52.3", + "@rollup/rollup-linux-arm64-musl": "4.52.3", + "@rollup/rollup-linux-loong64-gnu": "4.52.3", + "@rollup/rollup-linux-ppc64-gnu": "4.52.3", + "@rollup/rollup-linux-riscv64-gnu": "4.52.3", + "@rollup/rollup-linux-riscv64-musl": "4.52.3", + "@rollup/rollup-linux-s390x-gnu": "4.52.3", + "@rollup/rollup-linux-x64-gnu": "4.52.3", + "@rollup/rollup-linux-x64-musl": "4.52.3", + "@rollup/rollup-openharmony-arm64": "4.52.3", + "@rollup/rollup-win32-arm64-msvc": "4.52.3", + "@rollup/rollup-win32-ia32-msvc": "4.52.3", + "@rollup/rollup-win32-x64-gnu": "4.52.3", + "@rollup/rollup-win32-x64-msvc": "4.52.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz", + "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz", + "integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.22", + "@vue/compiler-sfc": "3.5.22", + "@vue/runtime-dom": "3.5.22", + "@vue/server-renderer": "3.5.22", + "@vue/shared": "3.5.22" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", + "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.1.0.tgz", + "integrity": "sha512-fbMynMG7kXSnqZTRBSCh9ROYaVpXfCZbEO0gY3lqOjLbp361uuS88n6BDajiUriDIF+SGLWoinjvf6stS2J3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.23", + "@vue/language-core": "3.1.0" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + } + } +} diff --git a/pmoupnp/webapp/package.json b/pmoupnp/webapp/package.json new file mode 100644 index 00000000..bffcc82e --- /dev/null +++ b/pmoupnp/webapp/package.json @@ -0,0 +1,25 @@ +{ + "name": "webapp", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "dompurify": "^3.2.7", + "marked": "^16.3.0", + "vue": "^3.5.21", + "vue-router": "^4.5.1" + }, + "devDependencies": { + "@types/dompurify": "^3.0.5", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/tsconfig": "^0.8.1", + "typescript": "~5.8.3", + "vite": "^7.1.7", + "vue-tsc": "^3.0.7" + } +} diff --git a/pmoupnp/webapp/public/vite.svg b/pmoupnp/webapp/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/pmoupnp/webapp/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pmoupnp/webapp/src/App.vue b/pmoupnp/webapp/src/App.vue new file mode 100644 index 00000000..f8a79926 --- /dev/null +++ b/pmoupnp/webapp/src/App.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/pmoupnp/webapp/src/assets/vue.svg b/pmoupnp/webapp/src/assets/vue.svg new file mode 100644 index 00000000..770e9d33 --- /dev/null +++ b/pmoupnp/webapp/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pmoupnp/webapp/src/components/HelloWorld.vue b/pmoupnp/webapp/src/components/HelloWorld.vue new file mode 100644 index 00000000..b58e52b9 --- /dev/null +++ b/pmoupnp/webapp/src/components/HelloWorld.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/pmoupnp/webapp/src/components/LogView.vue b/pmoupnp/webapp/src/components/LogView.vue new file mode 100644 index 00000000..e24fdd8b --- /dev/null +++ b/pmoupnp/webapp/src/components/LogView.vue @@ -0,0 +1,581 @@ + + + + + \ No newline at end of file diff --git a/pmoupnp/webapp/src/main.ts b/pmoupnp/webapp/src/main.ts new file mode 100644 index 00000000..0e93bac4 --- /dev/null +++ b/pmoupnp/webapp/src/main.ts @@ -0,0 +1,7 @@ +import { createApp } from "vue"; +import App from "./App.vue"; +import router from "./router"; + +import "./style.css"; + +createApp(App).use(router).mount("#app"); diff --git a/pmoupnp/webapp/src/router/index.ts b/pmoupnp/webapp/src/router/index.ts new file mode 100644 index 00000000..8cb9c97e --- /dev/null +++ b/pmoupnp/webapp/src/router/index.ts @@ -0,0 +1,16 @@ +import { createRouter, createWebHistory } from "vue-router"; +import HelloWorld from "../components/HelloWorld.vue"; +import LogView from "../components/LogView.vue"; + +const routes = [ + { path: "/", name: "home", component: HelloWorld }, + { path: "/logs", name: "logs", component: LogView }, +]; + +const router = createRouter({ + // history avec base /app + history: createWebHistory("/app"), + routes, +}); + +export default router; diff --git a/pmoupnp/webapp/src/shims-vue.d.ts b/pmoupnp/webapp/src/shims-vue.d.ts new file mode 100644 index 00000000..daba9b9e --- /dev/null +++ b/pmoupnp/webapp/src/shims-vue.d.ts @@ -0,0 +1,5 @@ +declare module "*.vue" { + import { DefineComponent } from "vue"; + const component: DefineComponent<{}, {}, any>; + export default component; +} diff --git a/pmoupnp/webapp/src/style.css b/pmoupnp/webapp/src/style.css new file mode 100644 index 00000000..1c69e469 --- /dev/null +++ b/pmoupnp/webapp/src/style.css @@ -0,0 +1,80 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; + width: 100vw; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +.card { + padding: 2em; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/pmoupnp/webapp/tsconfig.app.json b/pmoupnp/webapp/tsconfig.app.json new file mode 100644 index 00000000..8d16e425 --- /dev/null +++ b/pmoupnp/webapp/tsconfig.app.json @@ -0,0 +1,16 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "types": ["vite/client"], + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/pmoupnp/webapp/tsconfig.json b/pmoupnp/webapp/tsconfig.json new file mode 100644 index 00000000..1ffef600 --- /dev/null +++ b/pmoupnp/webapp/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/pmoupnp/webapp/tsconfig.node.json b/pmoupnp/webapp/tsconfig.node.json new file mode 100644 index 00000000..7a77bab3 --- /dev/null +++ b/pmoupnp/webapp/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": [], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/pmoupnp/webapp/vite.config.ts b/pmoupnp/webapp/vite.config.ts new file mode 100644 index 00000000..c7e0ac58 --- /dev/null +++ b/pmoupnp/webapp/vite.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [vue()], + base: '/app/', // Base path pour le déploiement +}) diff --git a/pmoutils/Cargo.toml b/pmoutils/Cargo.toml new file mode 100644 index 00000000..439ebc85 --- /dev/null +++ b/pmoutils/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "pmoutils" +version = "0.1.0" +edition = "2024" + +[dependencies] +get_if_addrs = "0.5.3" \ No newline at end of file diff --git a/pmoutils/src/ip_utils.rs b/pmoutils/src/ip_utils.rs new file mode 100644 index 00000000..d85c89ab --- /dev/null +++ b/pmoutils/src/ip_utils.rs @@ -0,0 +1,263 @@ +use get_if_addrs::get_if_addrs; +use std::net::UdpSocket; + +/// Devine l'adresse IP locale de la machine. +/// +/// Cette fonction tente de déterminer l'adresse IP locale en créant une connexion UDP +/// vers un serveur DNS public (8.8.8.8). Cette technique permet d'identifier l'interface +/// réseau qui serait utilisée pour communiquer avec Internet. +/// +/// # Fonctionnement +/// +/// 1. Crée un socket UDP lié à `0.0.0.0:0` (n'importe quelle interface, port aléatoire) +/// 2. Tente une connexion (non effective pour UDP) vers `8.8.8.8:80` +/// 3. Récupère l'adresse IP locale du socket +/// 4. En cas d'échec à n'importe quelle étape, retourne `127.0.0.1` +/// +/// # Returns +/// +/// Retourne l'adresse IP locale sous forme de `String`, ou `"127.0.0.1"` en cas d'erreur. +/// +/// # Examples +/// +/// ``` +/// let ip = guess_local_ip(); +/// println!("IP locale détectée: {}", ip); +/// // Affiche par exemple: "IP locale détectée: 192.168.1.42" +/// ``` +/// +/// # Note +/// +/// Cette méthode ne crée pas de véritable connexion réseau (UDP est sans connexion), +/// elle demande simplement au système d'exploitation quelle interface serait utilisée +/// pour joindre l'adresse cible. +pub fn guess_local_ip() -> String { + match UdpSocket::bind("0.0.0.0:0") { + Ok(socket) => { + if socket.connect("8.8.8.8:80").is_ok() { + if let Ok(local_addr) = socket.local_addr() { + return local_addr.ip().to_string(); + } + } + "127.0.0.1".to_string() + } + Err(_) => "127.0.0.1".to_string(), + } +} + +/// Liste toutes les adresses IP non-loopback des interfaces réseau. +/// +/// Parcourt toutes les interfaces réseau de la machine et collecte leurs adresses IPv4, +/// en excluant les adresses de loopback (127.0.0.1). +/// +/// # Returns +/// +/// Retourne une `HashMap` où : +/// - **Clé** : nom de l'interface réseau (ex: `"eth0"`, `"wlan0"`, `"en0"`) +/// - **Valeur** : vecteur des adresses IP (format String) associées à cette interface +/// +/// En cas d'erreur lors de la récupération des interfaces, retourne une HashMap +/// contenant une entrée `"error"` avec un message d'erreur. +/// +/// # Examples +/// +/// ``` +/// let ips = list_all_ips(); +/// for (interface, addresses) in ips { +/// println!("Interface {}: {:?}", interface, addresses); +/// } +/// // Affiche par exemple: +/// // Interface eth0: ["192.168.1.42"] +/// // Interface wlan0: ["10.0.0.15"] +/// ``` +/// +/// # Note +/// +/// - Seules les adresses IPv4 sont retournées +/// - Les adresses de loopback (127.x.x.x) sont filtrées +/// - Les adresses IPv6 sont ignorées +pub fn list_all_ips() -> std::collections::HashMap> { + let mut result = std::collections::HashMap::new(); + + if let Ok(interfaces) = get_if_addrs() { + for iface in interfaces { + let ip = iface.ip(); + if ip.is_loopback() { + continue; + } + if ip.is_ipv4() { + result + .entry(iface.name) + .or_insert_with(Vec::new) + .push(ip.to_string()); + } + } + } else { + result.insert( + "error".to_string(), + vec!["Failed to get interfaces".to_string()], + ); + } + + result +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::IpAddr; + + #[test] + fn test_guess_local_ip_returns_valid_ip() { + let ip = guess_local_ip(); + + // Vérifie que le résultat est parsable comme une IP + assert!(ip.parse::().is_ok(), "Should return a valid IP address"); + } + + #[test] + fn test_guess_local_ip_not_empty() { + let ip = guess_local_ip(); + + assert!(!ip.is_empty(), "IP should not be empty"); + } + + #[test] + fn test_guess_local_ip_is_ipv4() { + let ip = guess_local_ip(); + + if let Ok(parsed_ip) = ip.parse::() { + assert!(parsed_ip.is_ipv4(), "Should return an IPv4 address"); + } + } + + #[test] + fn test_guess_local_ip_fallback_is_localhost() { + // Ce test vérifie que si aucune IP n'est trouvée, on retourne 127.0.0.1 + // (difficile à tester sans mocker, mais on vérifie la cohérence) + let ip = guess_local_ip(); + let parsed = ip.parse::().unwrap(); + + // L'IP doit être soit locale (127.0.0.1) soit une IP privée valide + assert!( + parsed.is_loopback() || is_private_ip(&ip), + "IP should be either loopback or private" + ); + } + + #[test] + fn test_list_all_ips_no_loopback() { + let ips = list_all_ips(); + + // Vérifie qu'aucune adresse de loopback n'est présente + for (_, addresses) in ips.iter() { + for addr in addresses { + if let Ok(parsed_ip) = addr.parse::() { + assert!( + !parsed_ip.is_loopback(), + "Loopback addresses should be filtered out" + ); + } + } + } + } + + #[test] + fn test_list_all_ips_only_ipv4() { + let ips = list_all_ips(); + + // Vérifie que seules des adresses IPv4 sont retournées + for (iface_name, addresses) in ips.iter() { + if iface_name == "error" { + continue; // Skip error entries + } + + for addr in addresses { + if let Ok(parsed_ip) = addr.parse::() { + assert!( + parsed_ip.is_ipv4(), + "Only IPv4 addresses should be returned" + ); + } + } + } + } + + #[test] + fn test_list_all_ips_valid_format() { + let ips = list_all_ips(); + + // Vérifie que toutes les IPs sont dans un format valide + for (iface_name, addresses) in ips.iter() { + if iface_name == "error" { + continue; + } + + for addr in addresses { + assert!( + addr.parse::().is_ok(), + "Each IP should be in valid format: {}", + addr + ); + } + } + } + + #[test] + fn test_list_all_ips_interface_names_not_empty() { + let ips = list_all_ips(); + + // Vérifie que les noms d'interface ne sont pas vides + for (iface_name, _) in ips.iter() { + assert!(!iface_name.is_empty(), "Interface names should not be empty"); + } + } + + #[test] + fn test_list_all_ips_no_duplicate_ips_per_interface() { + let ips = list_all_ips(); + + // Vérifie qu'il n'y a pas de doublons par interface + for (iface_name, addresses) in ips.iter() { + if iface_name == "error" { + continue; + } + + let unique_addresses: std::collections::HashSet<_> = addresses.iter().collect(); + assert_eq!( + addresses.len(), + unique_addresses.len(), + "No duplicate IPs should exist for interface {}", + iface_name + ); + } + } + + // Fonction helper pour les tests + fn is_private_ip(ip_str: &str) -> bool { + if let Ok(ip) = ip_str.parse::() { + match ip { + IpAddr::V4(ipv4) => { + let octets = ipv4.octets(); + // Plages privées: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 + octets[0] == 10 + || (octets[0] == 172 && octets[1] >= 16 && octets[1] <= 31) + || (octets[0] == 192 && octets[1] == 168) + } + IpAddr::V6(_) => false, + } + } else { + false + } + } + + #[test] + fn test_helper_is_private_ip() { + // Tests pour la fonction helper + assert!(is_private_ip("10.0.0.1")); + assert!(is_private_ip("172.16.0.1")); + assert!(is_private_ip("192.168.1.1")); + assert!(!is_private_ip("8.8.8.8")); + assert!(!is_private_ip("127.0.0.1")); // loopback n'est pas "privé" au sens réseau local + } +} \ No newline at end of file diff --git a/pmoutils/src/lib.rs b/pmoutils/src/lib.rs new file mode 100644 index 00000000..e578b603 --- /dev/null +++ b/pmoutils/src/lib.rs @@ -0,0 +1,20 @@ +/// Utilitaires pour la gestion des adresses IP réseau. +/// +/// Ce module fournit des fonctions pour détecter et lister les adresses IP +/// des interfaces réseau locales de la machine. +/// +/// # Fonctions principales +/// +/// - [`guess_local_ip`] : Devine l'adresse IP locale utilisée pour les connexions sortantes +/// +/// # Examples +/// +/// ``` +/// use votre_crate::guess_local_ip; +/// +/// let ip = guess_local_ip(); +/// println!("Adresse IP locale: {}", ip); +/// ``` +mod ip_utils; + +pub use ip_utils::guess_local_ip; \ No newline at end of file diff --git a/structure.svg b/structure.svg new file mode 100644 index 00000000..e69de29b diff --git a/tools/build_prompt b/tools/build_prompt index 8b09b058..9e988b1c 100755 --- a/tools/build_prompt +++ b/tools/build_prompt @@ -1,26 +1,32 @@ #!/bin/bash -cat << EOF -Est-ce que ces packages te semblent fonctionnel. -Il ne s'agit pas de l'enrichir de nouvelle fonctionnalité, ni de l'optimiser d'avantage. Juste de le finaliser en detectant vrais bug. Ce package doit être thread safe, uniquement stéréo. -Ne signale que les bugs conduisant à un disfonctionnement - -Regénère des version complètes et corrigées des fichiers nécessaires - -EOF - -echo ============== Debut des sources des packages =============== +echo "# Debut des sources des crates " +echo for package in $*; do - pushd $package 2>&1 >/dev/null - for f in *.go; do - echo "------- $package/$f ------" - cat $f - echo "-----------------" + find $package -type f \( -name '*.rs' -o -name '*.go' \ + -o -name '*.js' -o -name '*.ts' \ + -o -name '*.vue' -o -name '*.css' \ + -o -name '*.toml' \) -print \ +| grep -v 'webapp/[^s]' \ +| while read -r file; do + case "$file" in + *.rs) language="rust" ;; + *.go) language="go" ;; + *.js) lanquage="javascript" ;; + *.ts) language="typescript" ;; + *.vue) language="vue" ;; + *.css) language="css" ;; + *.toml) language="toml" ;; + *) language="" ;; + esac + + echo "## fichier: \`$file\`" + echo + echo "\`\`\`$language" + cat $file + echo "\`\`\`" + echo done - popd 2>&1 >/dev/null done -echo ============== Fin des sources des packages =============== - -echo ============== Analyse des bugs par chatgpt ===============