diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 00000000..e3df146e --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,31 @@ +# PMOMusic Project Configuration + +## Version Control +Ce projet utilise **Jujutsu (jj)** pour le contrôle de version, PAS git. +- Utiliser les commandes `jj` au lieu des commandes `git` +- Bookmark principal : `main` +- Ne jamais suggérer de commandes git + +## Environnement +Le PATH et les variables d'environnement sont configurés dans `.claude-env` à la racine du projet. + +## Configuration de l'application +- Fichier de configuration principal : `.pmomusic/config.yaml` +- Configuration UPNP personnalisable pour différencier les instances en développement + +## Développement +Pendant le développement, plusieurs serveurs PMOMusic peuvent tourner en parallèle. Utiliser la configuration UPNP dans `.pmomusic/config.yaml` pour différencier les instances : + +```yaml +host: + upnp: + manufacturer: "PMOMusic-Dev1" + udn_prefix: "pmomusic-dev1" + model_name_prefix: "PMOMusic-Dev1" + friendly_name_prefix: "PMOMusic-Dev1" +``` + +## Architecture +- Projet Rust multi-crates avec workspaces +- Crates principales : pmoupnp, pmomediaserver, pmomediarenderer, pmoconfig +- Pattern d'extension de configuration via traits (voir pmocache/src/config_ext.rs) diff --git a/Cargo.lock b/Cargo.lock index 3736cee7..16b7238d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4021,6 +4021,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "serde_yaml", "socket2 0.5.10", "thiserror 2.0.17", "tokio", diff --git a/pmoconfig/src/pmomusic.yaml b/pmoconfig/src/pmomusic.yaml index 6423ad4e..0272b677 100644 --- a/pmoconfig/src/pmomusic.yaml +++ b/pmoconfig/src/pmomusic.yaml @@ -1,5 +1,10 @@ host: http_port: "8080" + upnp: + manufacturer: "PMOMusic" + udn_prefix: "pmomusic" + model_name_prefix: "PMOMusic" + friendly_name_prefix: "PMOMusic" cover_cache: directory: "cache_covers" size: 2000 diff --git a/pmomediarenderer/src/device.rs b/pmomediarenderer/src/device.rs index 2d30d4c2..43a94241 100644 --- a/pmomediarenderer/src/device.rs +++ b/pmomediarenderer/src/device.rs @@ -41,16 +41,13 @@ use pmoupnp::devices::Device; /// } /// ``` pub static MEDIA_RENDERER: Lazy> = Lazy::new(|| { - let mut device = Device::new( + let mut device = Device::new_from_config( "PMO_MediaRenderer".to_string(), "MediaRenderer".to_string(), - "PMOMusic Audio Renderer".to_string(), + "Audio Renderer".to_string(), ); - device.set_manufacturer("PMOMusic".to_string()); - device.set_model_name("PMOMusic Audio Renderer".to_string()); device.set_model_description("UPnP AV MediaRenderer for audio streaming".to_string()); - device.set_udn_prefix("pmomusic".to_string()); // Ajouter les trois services obligatoires device diff --git a/pmomediaserver/src/device.rs b/pmomediaserver/src/device.rs index 9ea04a24..aba871c4 100644 --- a/pmomediaserver/src/device.rs +++ b/pmomediaserver/src/device.rs @@ -37,16 +37,13 @@ use pmoupnp::devices::Device; /// } /// ``` pub static MEDIA_SERVER: Lazy> = Lazy::new(|| { - let mut device = Device::new( + let mut device = Device::new_from_config( "PMO_MediaServer".to_string(), "MediaServer".to_string(), - "PMOMusic Media Server".to_string(), + "Media Server".to_string(), ); - device.set_manufacturer("PMOMusic".to_string()); - device.set_model_name("PMOMusic Media Server".to_string()); device.set_model_description("UPnP AV MediaServer for audio streaming".to_string()); - device.set_udn_prefix("pmomusic".to_string()); // Ajouter les deux services obligatoires device diff --git a/pmoupnp/Cargo.toml b/pmoupnp/Cargo.toml index 9f41ffd1..9b6f3be3 100644 --- a/pmoupnp/Cargo.toml +++ b/pmoupnp/Cargo.toml @@ -36,6 +36,7 @@ reqwest = "0.12.23" utoipa = { version = "5.3", features = ["axum_extras"] } socket2 = "0.5" get_if_addrs = "0.5" +serde_yaml = "0.9" [features] default = ["server"] diff --git a/pmoupnp/src/devices/device.rs b/pmoupnp/src/devices/device.rs index d947c9ea..1a2190e6 100644 --- a/pmoupnp/src/devices/device.rs +++ b/pmoupnp/src/devices/device.rs @@ -122,6 +122,84 @@ impl Device { } } + /// Crée un nouveau modèle de device en utilisant la configuration. + /// + /// Cette factory method charge les préfixes depuis pmoconfig et construit + /// automatiquement les noms finaux en combinant les préfixes avec les suffixes. + /// + /// # Arguments + /// + /// * `name` - Nom unique du device + /// * `device_type` - Type UPnP du device (ex: "MediaServer", "MediaRenderer") + /// * `friendly_name_suffix` - Suffixe pour le nom convivial (sera combiné avec le préfixe) + /// + /// # Examples + /// + /// ```ignore + /// use pmoupnp::devices::Device; + /// + /// let device = Device::new_from_config( + /// "PMO_MediaServer".to_string(), + /// "MediaServer".to_string(), + /// "Media Server".to_string(), + /// ); + /// // Avec config par défaut : + /// // - manufacturer = "PMOMusic" + /// // - udn_prefix = "pmomusic" + /// // - model_name = "PMOMusic Media Server" + /// // - friendly_name = "PMOMusic Media Server" + /// ``` + pub fn new_from_config( + name: String, + device_type: String, + friendly_name_suffix: String, + ) -> Self { + use crate::config_ext::UpnpConfigExt; + + let config = pmoconfig::get_config(); + + // Charger les valeurs depuis la config (avec fallback aux defaults) + let manufacturer = config + .get_upnp_manufacturer() + .unwrap_or_else(|_| "PMOMusic".to_string()); + let udn_prefix = config + .get_upnp_udn_prefix() + .unwrap_or_else(|_| "pmomusic".to_string()); + let model_name_prefix = config + .get_upnp_model_name_prefix() + .unwrap_or_else(|_| "PMOMusic".to_string()); + let friendly_name_prefix = config + .get_upnp_friendly_name_prefix() + .unwrap_or_else(|_| "PMOMusic".to_string()); + + // Construire les noms finaux + let model_name = format!("{} {}", model_name_prefix, device_type); + let friendly_name = format!("{} {}", friendly_name_prefix, friendly_name_suffix); + + Self { + object: UpnpObjectType { + name: name.clone(), + object_type: "Device".to_string(), + }, + device_type, + version: 1, + friendly_name, + manufacturer, + manufacturer_url: None, + model_description: None, + model_name, + model_number: None, + model_url: None, + serial_number: None, + udn_prefix, + upc: None, + icon_url: None, + presentation_url: None, + services: RwLock::new(HashMap::new()), + devices: RwLock::new(HashMap::new()), + } + } + /// Retourne le type de device UPnP. /// /// Format: `urn:schemas-upnp-org:device:{type}:{version}` diff --git a/pmoupnp/src/upnp_server.rs b/pmoupnp/src/upnp_server.rs index e76ccf40..64876e9a 100644 --- a/pmoupnp/src/upnp_server.rs +++ b/pmoupnp/src/upnp_server.rs @@ -261,7 +261,12 @@ impl UpnpServerExt for Server { if self.ssdp_enabled() { let ssdp_opt = SSDP_SERVER.read().unwrap(); if let Some(ref ssdp) = *ssdp_opt { - let ssdp_device = di.to_ssdp_device("PMOMusic", "1.0"); + use crate::config_ext::UpnpConfigExt; + let config = pmoconfig::get_config(); + let manufacturer = config + .get_upnp_manufacturer() + .unwrap_or_else(|_| "PMOMusic".to_string()); + let ssdp_device = di.to_ssdp_device(&manufacturer, "1.0"); ssdp.add_device(ssdp_device); info!("✅ SSDP announcement for {}", di.udn()); }