From 2eec78ab4fb216ede367885c418c8204f95f646b Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Fri, 17 Oct 2025 13:12:28 +0200 Subject: [PATCH] ashboard dans le webapp qui liste dynamiquement toutes les APIs OpenAPI disponibles dans PMOMusic --- .pmomusic.yml | 16 +- Cargo.lock | 1 + Cargo.toml | 20 +- PMOMusic/Cargo.toml | 1 + PMOMusic/src/main.rs | 15 +- audio_cache/audio_cache.db | Bin 0 -> 20480 bytes pmoapp/webapp/src/App.vue | 4 +- pmoapp/webapp/src/components/APIDashboard.vue | 449 ++++++++++ pmoapp/webapp/src/components/APIExplorer.vue | 802 ------------------ pmoapp/webapp/src/router/index.ts | 4 +- pmoconfig/src/pmomusic.yaml | 2 +- pmomediarenderer/src/device.rs | 2 +- pmomediaserver/src/device.rs | 2 +- pmoserver/src/lib.rs | 2 +- pmoserver/src/server.rs | 85 +- pmoupnp/src/devices/device.rs | 4 + pmoupnp/src/devices/device_instance.rs | 4 +- pmoupnp/src/lib.rs | 2 +- pmoupnp/src/upnp_server.rs | 4 +- 19 files changed, 588 insertions(+), 831 deletions(-) create mode 100644 audio_cache/audio_cache.db create mode 100644 pmoapp/webapp/src/components/APIDashboard.vue delete mode 100644 pmoapp/webapp/src/components/APIExplorer.vue diff --git a/.pmomusic.yml b/.pmomusic.yml index 2da6807e..6440da8c 100644 --- a/.pmomusic.yml +++ b/.pmomusic.yml @@ -10,19 +10,19 @@ host: buffer_capacity: 200 enable_console: true min_level: INFO -devices: mediarenderer: mpd_renderer: null - fakerenderer: - udn: d7eaad15-7d21-4411-926a-bc1eea0713db - mediarenderer: - udn: f9ef6c21-0ed3-470c-9846-bc1ae85fea62 - mediaserver: - udn: 4aa1d843-22ca-4a1d-b383-43a9d197d875 mediaserver: qobuz: - udn: 28963b75-4c5f-4da7-b10e-ffafd + udn: uuid:28963b75-4c5f-4da7-b10e-ffafd accounts: qobuz: username: eric@coissac.eu password: '*Misfcr73110$' +devices: + mediarenderer: + pmo_mediarenderer: + udn: 15a13316-daac-47f0-b64e-47e56f5e3b51 + mediaserver: + pmo_mediaserver: + udn: 23df0bfa-cfef-4724-b731-00f66fadf176 diff --git a/Cargo.lock b/Cargo.lock index 93964bd3..c4609cb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,7 @@ version = "0.1.0" dependencies = [ "axum", "pmoapp", + "pmoaudiocache", "pmoconfig", "pmocovers", "pmomediarenderer", diff --git a/Cargo.toml b/Cargo.toml index 3ea3095c..cb1b2331 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,21 @@ [workspace] resolver = "3" -members = ["PMOMusic", "pmoupnp", "pmomediarenderer", "pmomediaserver", "pmoconfig", "pmoutils", "pmodidl", "pmoserver", "pmoapp", "pmocache", "pmocovers", "pmoaudiocache", "pmoaudio", "pmoqobuz", "pmoparadise", "pmosource", "pmoplaylist"] +members = [ + "PMOMusic", + "pmoupnp", + "pmomediarenderer", + "pmomediaserver", + "pmoconfig", + "pmoutils", + "pmodidl", + "pmoserver", + "pmoapp", + "pmocache", + "pmocovers", + "pmoaudiocache", + "pmoaudio", + "pmoqobuz", + "pmoparadise", + "pmosource", + "pmoplaylist", +] diff --git a/PMOMusic/Cargo.toml b/PMOMusic/Cargo.toml index bf4a86b3..c8698c1a 100644 --- a/PMOMusic/Cargo.toml +++ b/PMOMusic/Cargo.toml @@ -11,6 +11,7 @@ pmomediaserver = { path = "../pmomediaserver", features = ["qobuz", "paradise", pmosource = { path = "../pmosource", features = ["server"] } pmoserver = { path = "../pmoserver" } pmocovers = { path = "../pmocovers", features = ["pmoserver"] } +pmoaudiocache = { path = "../pmoaudiocache", features = ["pmoserver"]} pmoapp = { path = "../pmoapp", features = ["pmoserver"] } diff --git a/PMOMusic/src/main.rs b/PMOMusic/src/main.rs index a9440dcb..223b4d80 100644 --- a/PMOMusic/src/main.rs +++ b/PMOMusic/src/main.rs @@ -1,10 +1,11 @@ use pmoapp::{WebAppExt, Webapp}; use pmocovers::CoverCacheExt; +use pmoaudiocache::AudioCacheExt; use pmomediarenderer::MEDIA_RENDERER; use pmomediaserver::{MEDIA_SERVER, sources::SourcesExt}; use pmosource::MusicSourceExt; use pmoserver::ServerBuilder; -use pmoupnp::{UpnpServer, ssdp::SsdpServer, upnp_api::UpnpApiExt}; +use pmoupnp::{UpnpServerExt, ssdp::SsdpServer, upnp_api::UpnpApiExt}; use tracing::info; #[tokio::main] @@ -16,12 +17,20 @@ async fn main() { server.init_logging().await; info!("📡 Registering the cover cache..."); - let cache = server + let covercache = server .init_cover_cache_configured() .await .expect("Cannot initialise the image cache"); - info!("✅ Cover cache ready at {}", cache.cache_dir(),); + info!("✅ Cover cache ready at {}", covercache.cache_dir(),); + + info!("📡 Registering the audio cache..."); + let audiocache = server + .init_audio_cache_configured() + .await + .expect("Cannot initialise the audio cache"); + + info!("✅ Audio cache ready at {}", audiocache.cache_dir(),); // Routes de base server diff --git a/audio_cache/audio_cache.db b/audio_cache/audio_cache.db new file mode 100644 index 0000000000000000000000000000000000000000..68743070233d8e4b4ff8c729cc37274f18393597 GIT binary patch literal 20480 zcmeI#O>fgM7{GC7?Orq~Y`0#0sYWG)m^g5RvYDw`8Bl_>afz}dKvD(p=4x0J+=p4g9nb`E~~W*nJNen_)(&&bDOTUeHOCZ!OfQ(UXXRhP2e ztsBMDYSi1R9r5sbYH$4zU3(<%oo?OO--`+b0R#|0009ILKmY**5cr=3W_EYIx7W93 zPopFVucF|}_3|J}U6XnKnRfj&xd<~IrAhPk>fS^R9VJIc`|4Q!6vuYu_IqkMlXkx6M3jk9xMduG|&9d&9k@)UJUTlI?0xRPm$5Z z(m3isP2|3MF?=_6📋 Logs 🎵 UPnP Explorer 🎨 Cover Cache - 🌐 API Explorer + 🚀 API Dashboard @@ -31,7 +31,7 @@ const showDebugMenu = ref(false) const route = useRoute() const isDebugRoute = computed(() => { - return ['/logs', '/upnp', '/covers-cache', '/api-explorer'].includes(route.path) + return ['/logs', '/upnp', '/covers-cache', '/api-dashboard'].includes(route.path) }) diff --git a/pmoapp/webapp/src/components/APIDashboard.vue b/pmoapp/webapp/src/components/APIDashboard.vue new file mode 100644 index 00000000..c73f11f6 --- /dev/null +++ b/pmoapp/webapp/src/components/APIDashboard.vue @@ -0,0 +1,449 @@ + + + + + diff --git a/pmoapp/webapp/src/components/APIExplorer.vue b/pmoapp/webapp/src/components/APIExplorer.vue deleted file mode 100644 index dc48973b..00000000 --- a/pmoapp/webapp/src/components/APIExplorer.vue +++ /dev/null @@ -1,802 +0,0 @@ - - - - - diff --git a/pmoapp/webapp/src/router/index.ts b/pmoapp/webapp/src/router/index.ts index 2e6c3793..5b505243 100644 --- a/pmoapp/webapp/src/router/index.ts +++ b/pmoapp/webapp/src/router/index.ts @@ -3,14 +3,14 @@ import HelloWorld from "../components/HelloWorld.vue"; import LogView from "../components/LogView.vue"; import CoverCacheManager from "../components/CoverCacheManager.vue"; import UpnpExplorer from "../components/UpnpExplorer.vue"; -import APIExplorer from "../components/APIExplorer.vue"; +import APIDashboard from "../components/APIDashboard.vue"; const routes = [ { path: "/", name: "home", component: HelloWorld }, { path: "/logs", name: "logs", component: LogView }, { path: "/covers-cache", name: "covers-cache", component: CoverCacheManager }, { path: "/upnp", name: "upnp", component: UpnpExplorer }, - { path: "/api-explorer", name: "api-explorer", component: APIExplorer }, + { path: "/api-dashboard", name: "api-dashboard", component: APIDashboard }, ]; const router = createRouter({ diff --git a/pmoconfig/src/pmomusic.yaml b/pmoconfig/src/pmomusic.yaml index d584d632..ee610894 100644 --- a/pmoconfig/src/pmomusic.yaml +++ b/pmoconfig/src/pmomusic.yaml @@ -10,7 +10,7 @@ host: buffer_capacity: 200 enable_console: true min_level: "INFO" -devices: + mediarenderer: mpd_renderer: mediaserver: diff --git a/pmomediarenderer/src/device.rs b/pmomediarenderer/src/device.rs index 94977d82..b8c9df46 100644 --- a/pmomediarenderer/src/device.rs +++ b/pmomediarenderer/src/device.rs @@ -43,7 +43,7 @@ use crate::{ /// ``` pub static MEDIA_RENDERER: Lazy> = Lazy::new(|| { let mut device = Device::new( - "MediaRenderer".to_string(), + "PMO_MediaRenderer".to_string(), "MediaRenderer".to_string(), "PMOMusic Audio Renderer".to_string(), ); diff --git a/pmomediaserver/src/device.rs b/pmomediaserver/src/device.rs index 7ca6da16..12c54bc6 100644 --- a/pmomediaserver/src/device.rs +++ b/pmomediaserver/src/device.rs @@ -41,7 +41,7 @@ use crate::{ /// ``` pub static MEDIA_SERVER: Lazy> = Lazy::new(|| { let mut device = Device::new( - "MediaServer".to_string(), + "PMO_MediaServer".to_string(), "MediaServer".to_string(), "PMOMusic Media Server".to_string(), ); diff --git a/pmoserver/src/lib.rs b/pmoserver/src/lib.rs index 71689fa5..7f9c475f 100644 --- a/pmoserver/src/lib.rs +++ b/pmoserver/src/lib.rs @@ -71,5 +71,5 @@ pub mod server; pub mod logs; -pub use server::{Server, ServerBuilder, ServerInfo}; +pub use server::{Server, ServerBuilder, ServerInfo, ApiRegistry, ApiRegistryEntry}; pub use logs::{LogState, SseLayer, log_sse, log_dump, init_logging, LoggingOptions, log_setup_get, log_setup_post}; diff --git a/pmoserver/src/server.rs b/pmoserver/src/server.rs index f8ee41e6..eb27b2ce 100644 --- a/pmoserver/src/server.rs +++ b/pmoserver/src/server.rs @@ -13,7 +13,8 @@ //! - 📚 **Documentation API** : OpenAPI/Swagger automatique avec `add_openapi()` //! - ⚡ **Gestion gracieuse** : Arrêt propre sur Ctrl+C -use crate::logs::{LogState, LoggingOptions, init_logging, log_dump, log_sse}; +use crate::logs::{LogState, init_logging, log_dump, log_sse}; +use axum::extract::State; use axum::handler::Handler; use axum::response::Redirect; use axum::routing::{get, post}; @@ -37,6 +38,49 @@ pub struct ServerInfo { pub http_port: u16, } +/// Entrée du registre d'API +#[derive(Clone, Serialize, utoipa::ToSchema)] +pub struct ApiRegistryEntry { + /// Nom de l'API + pub name: String, + /// Chemin de base de l'API + pub path: String, + /// Chemin vers Swagger UI + pub swagger_ui_path: String, + /// Chemin vers le JSON OpenAPI + pub openapi_json_path: String, + /// Nombre d'endpoints + pub endpoint_count: usize, + /// Version de l'API + pub version: String, + /// Description de l'API + pub description: Option, + /// Titre de l'API + pub title: String, +} + +/// Liste des APIs enregistrées +#[derive(Clone, Serialize, utoipa::ToSchema)] +pub struct ApiRegistry { + /// Liste des APIs disponibles + pub apis: Vec, + /// Nombre total d'endpoints + pub total_endpoints: usize, +} + +type ApiRegistryState = Arc>>; + +/// Handler pour l'endpoint /api/registry +async fn get_api_registry(State(registry): State) -> Json { + let apis = registry.read().await.clone(); + let total_endpoints = apis.iter().map(|api| api.endpoint_count).sum(); + + Json(ApiRegistry { + apis, + total_endpoints, + }) +} + /// Serveur principal pub struct Server { name: String, @@ -46,6 +90,7 @@ pub struct Server { api_router: Arc>>, join_handle: Option>, log_state: Option, + api_registry: ApiRegistryState, } impl Server { @@ -64,14 +109,22 @@ impl Server { /// let server = Server::new("MyAPI", "http://localhost:3000", 3000); /// ``` pub fn new(name: impl Into, base_url: impl Into, http_port: u16) -> Self { + let api_registry = Arc::new(RwLock::new(Vec::new())); + + // Créer le router initial avec l'endpoint de registre + let registry_route = Router::new() + .route("/api/registry", get(get_api_registry)) + .with_state(api_registry.clone()); + Self { name: name.into(), base_url: base_url.into(), http_port, - router: Arc::new(RwLock::new(Router::new())), + router: Arc::new(RwLock::new(registry_route)), api_router: Arc::new(RwLock::new(None)), join_handle: None, log_state: None, + api_registry, } } @@ -378,10 +431,34 @@ impl Server { drop(api_r); let swagger_path = format!("/swagger-ui/{}", name); - let swagger_path_static: &'static str = Box::leak(swagger_path.into_boxed_str()); + let swagger_path_static: &'static str = Box::leak(swagger_path.clone().into_boxed_str()); let openapi_json_path = format!("/api-docs/{}.json", name); - let openapi_json_path_static: &'static str = Box::leak(openapi_json_path.into_boxed_str()); + let openapi_json_path_static: &'static str = Box::leak(openapi_json_path.clone().into_boxed_str()); + + // Compter le nombre d'endpoints dans l'OpenAPI spec + let endpoint_count = openapi.paths.paths.len(); + + // Extraire les informations de l'API depuis la spec OpenAPI + let version = openapi.info.version.clone(); + let description = openapi.info.description.clone(); + let title = openapi.info.title.clone(); + + // Enregistrer l'API dans le registre + let registry_entry = ApiRegistryEntry { + name: name.to_string(), + path: format!("/api/{}", name), + swagger_ui_path: swagger_path, + openapi_json_path, + endpoint_count, + version, + description, + title, + }; + + let mut registry = self.api_registry.write().await; + registry.push(registry_entry); + drop(registry); let swagger = SwaggerUi::new(swagger_path_static).url(openapi_json_path_static, openapi); diff --git a/pmoupnp/src/devices/device.rs b/pmoupnp/src/devices/device.rs index eca7e7bc..268f6e47 100644 --- a/pmoupnp/src/devices/device.rs +++ b/pmoupnp/src/devices/device.rs @@ -132,6 +132,10 @@ impl Device { format!("urn:schemas-upnp-org:device:{}:{}", self.device_type, self.version) } + pub fn device_category(&self) -> &String { + &self.device_type + } + /// Définit la version du device. pub fn set_version(&mut self, version: u8) -> Result<(), DeviceError> { if version == 0 { diff --git a/pmoupnp/src/devices/device_instance.rs b/pmoupnp/src/devices/device_instance.rs index 35647cec..cc7d59b3 100644 --- a/pmoupnp/src/devices/device_instance.rs +++ b/pmoupnp/src/devices/device_instance.rs @@ -78,8 +78,8 @@ impl UpnpInstance for DeviceInstance { fn new(model: &Device) -> Self { // Obtenir ou créer un UDN persistant via la configuration let device_name = model.get_name(); - - let udn = if let Ok(config_udn) = pmoconfig::get_config().get_device_udn("mediarenderer", device_name) { + let device_type = model.device_category(); + let udn = if let Ok(config_udn) = pmoconfig::get_config().get_device_udn(&device_type, device_name) { config_udn } else { // Fallback : générer un UDN diff --git a/pmoupnp/src/lib.rs b/pmoupnp/src/lib.rs index 94f0fcc8..f845019e 100644 --- a/pmoupnp/src/lib.rs +++ b/pmoupnp/src/lib.rs @@ -20,7 +20,7 @@ use std::{collections::HashMap, sync::Arc}; use std::sync::RwLock; pub use crate::object_trait::*; -pub use crate::upnp_server::UpnpServer; +pub use crate::upnp_server::UpnpServerExt; #[derive(Debug, Clone)] pub struct UpnpObjectType { diff --git a/pmoupnp/src/upnp_server.rs b/pmoupnp/src/upnp_server.rs index 994c9b98..d9726203 100644 --- a/pmoupnp/src/upnp_server.rs +++ b/pmoupnp/src/upnp_server.rs @@ -69,7 +69,7 @@ static DEVICE_REGISTRY: Lazy> = Lazy::new(|| { /// // Introspection via le trait UpnpServer /// let devices = server.device_registry().list_devices(); /// ``` -pub trait UpnpServer { +pub trait UpnpServerExt { /// Enregistre un device UPnP et toutes ses URLs. /// /// # Arguments @@ -92,7 +92,7 @@ pub trait UpnpServer { } // Implémentation du trait UpnpServer pour pmoserver::Server -impl UpnpServer for Server { +impl UpnpServerExt for Server { async fn register_device(&mut self, device: Arc) -> Result, DeviceError> { // Créer l'instance (retourne déjà un Arc) let di = device.create_instance();