From cc3e31dbd04e4c7c7b75f422260823f17ec22661 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Nov 2025 11:10:17 +0000 Subject: [PATCH 1/4] fix: Eliminate channel/block_base duplication in ClientBuilder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le bug identifié était que le block_base n'était pas synchronisé avec le channel dans le ClientBuilder, causant le téléchargement du même bloc pour différents channels. Changements: - Supprimé le champ block_base du ClientBuilder (duplication) - Supprimé la constante DEFAULT_BLOCK_BASE (plus nécessaire) - Supprimé la méthode .block_base() du builder (complexité inutile) - Le block_base est maintenant calculé dynamiquement dans build() à partir du channel, éliminant toute possibilité de désynchronisation Cette approche suit le principe DRY et élimine une source de bugs. --- pmoparadise/src/client.rs | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/pmoparadise/src/client.rs b/pmoparadise/src/client.rs index 3ba5ca94..2f6b7795 100644 --- a/pmoparadise/src/client.rs +++ b/pmoparadise/src/client.rs @@ -9,9 +9,6 @@ use url::Url; /// Default Radio Paradise API base URL pub const DEFAULT_API_BASE: &str = "https://api.radioparadise.com/api"; -/// Default block base URL pattern -pub const DEFAULT_BLOCK_BASE: &str = "https://apps.radioparadise.com/blocks/chan/0"; - /// Default image base URL pub const DEFAULT_IMAGE_BASE: &str = "https://img.radioparadise.com/"; @@ -72,11 +69,12 @@ impl RadioParadiseClient { /// /// Useful for sharing HTTP connection pools or custom proxy settings pub fn with_client(client: Client) -> Self { + let channel = 0; Self { client, api_base: DEFAULT_API_BASE.to_string(), - block_base: DEFAULT_BLOCK_BASE.to_string(), - channel: 0, + block_base: Self::block_base_for_channel(channel), + channel, request_timeout: Duration::from_secs(DEFAULT_REQUEST_TIMEOUT_SECS), block_timeout: Duration::from_secs(DEFAULT_BLOCK_TIMEOUT_SECS), next_block_url: None, @@ -234,7 +232,6 @@ impl RadioParadiseClient { pub struct ClientBuilder { client: Option, api_base: String, - block_base: String, channel: u8, request_timeout: Duration, block_timeout: Duration, @@ -247,7 +244,6 @@ impl Default for ClientBuilder { Self { client: None, api_base: DEFAULT_API_BASE.to_string(), - block_base: DEFAULT_BLOCK_BASE.to_string(), channel: 0, request_timeout: Duration::from_secs(DEFAULT_REQUEST_TIMEOUT_SECS), block_timeout: Duration::from_secs(DEFAULT_BLOCK_TIMEOUT_SECS), @@ -275,15 +271,10 @@ impl ClientBuilder { self } - /// Set the block base URL - pub fn block_base(mut self, url: impl Into) -> Self { - self.block_base = url.into(); - self - } - /// Set the channel (0 = main mix, 1 = mellow, 2 = rock, 3 = world/etc) pub fn channel(mut self, channel: u8) -> Self { self.channel = channel; + // block_base sera calculé dynamiquement dans build() à partir du channel self } @@ -329,11 +320,8 @@ impl ClientBuilder { builder.build()? }; - let block_base = if self.block_base == DEFAULT_BLOCK_BASE { - RadioParadiseClient::block_base_for_channel(self.channel) - } else { - self.block_base.clone() - }; + // Calculer block_base dynamiquement à partir du channel + let block_base = RadioParadiseClient::block_base_for_channel(self.channel); Ok(RadioParadiseClient { client, From 0a54db59639fc4383de9b24b10e69c118a733149 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Nov 2025 11:13:26 +0000 Subject: [PATCH 2/4] refactor: Replace block_base field with dynamic calculation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supprime complètement la duplication d'information en transformant block_base d'un champ stocké en une méthode calculée dynamiquement. Changements: - Supprimé le champ block_base de RadioParadiseClient - Ajouté la constante BLOCK_BASE_URL pour éviter la duplication de l'URL - Transformé block_base en méthode publique qui calcule à partir de channel - Simplifié with_client() et clone_with_channel() - Simplifié le builder qui n'a plus besoin d'initialiser block_base Cette approche garantit que block_base est toujours cohérent avec channel, éliminant définitivement toute possibilité de bug de synchronisation. --- pmoparadise/src/client.rs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/pmoparadise/src/client.rs b/pmoparadise/src/client.rs index 2f6b7795..3e6e4b4e 100644 --- a/pmoparadise/src/client.rs +++ b/pmoparadise/src/client.rs @@ -9,6 +9,9 @@ use url::Url; /// Default Radio Paradise API base URL pub const DEFAULT_API_BASE: &str = "https://api.radioparadise.com/api"; +/// Default block base URL (channel is appended) +pub const DEFAULT_BLOCK_BASE: &str = "https://apps.radioparadise.com/blocks/chan"; + /// Default image base URL pub const DEFAULT_IMAGE_BASE: &str = "https://img.radioparadise.com/"; @@ -45,7 +48,6 @@ pub const DEFAULT_USER_AGENT: &str = "pmoparadise/0.1.0"; pub struct RadioParadiseClient { pub(crate) client: Client, api_base: String, - block_base: String, channel: u8, pub(crate) request_timeout: Duration, pub(crate) block_timeout: Duration, @@ -69,12 +71,10 @@ impl RadioParadiseClient { /// /// Useful for sharing HTTP connection pools or custom proxy settings pub fn with_client(client: Client) -> Self { - let channel = 0; Self { client, api_base: DEFAULT_API_BASE.to_string(), - block_base: Self::block_base_for_channel(channel), - channel, + channel: 0, request_timeout: Duration::from_secs(DEFAULT_REQUEST_TIMEOUT_SECS), block_timeout: Duration::from_secs(DEFAULT_BLOCK_TIMEOUT_SECS), next_block_url: None, @@ -86,15 +86,15 @@ impl RadioParadiseClient { self.channel } - fn block_base_for_channel(channel: u8) -> String { - format!("https://apps.radioparadise.com/blocks/chan/{}", channel) + /// Get the block base URL for this client's channel + pub fn block_base(&self) -> String { + format!("{}/{}", DEFAULT_BLOCK_BASE, self.channel) } /// Clone the client with a different channel while preserving other settings. pub fn clone_with_channel(&self, channel: u8) -> Self { let mut cloned = self.clone(); cloned.channel = channel; - cloned.block_base = Self::block_base_for_channel(channel); cloned.next_block_url = None; cloned } @@ -320,13 +320,9 @@ impl ClientBuilder { builder.build()? }; - // Calculer block_base dynamiquement à partir du channel - let block_base = RadioParadiseClient::block_base_for_channel(self.channel); - Ok(RadioParadiseClient { client, api_base: self.api_base, - block_base, channel: self.channel, request_timeout: self.request_timeout, block_timeout: self.block_timeout, From bac4a94cade925d4feb3c4011c2b1611a46453f3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Nov 2025 11:18:11 +0000 Subject: [PATCH 3/4] refactor: Eliminate remaining duplications in client.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Corrections : 1. Supprimé le commentaire obsolète sur block_base (ligne 277) 2. Créé la constante DEFAULT_CHANNEL pour éviter de coder "0" en dur 3. Utilisé DEFAULT_CHANNEL dans with_client(), ClientBuilder::default() et tests 4. Amélioré la documentation de with_client() pour guider vers le builder Bien que with_client() et ClientBuilder::default() aient encore une structure similaire, ils utilisent maintenant les mêmes constantes, réduisant ainsi le risque d'incohérence lors de modifications futures. --- pmoparadise/src/client.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pmoparadise/src/client.rs b/pmoparadise/src/client.rs index 3e6e4b4e..48b87295 100644 --- a/pmoparadise/src/client.rs +++ b/pmoparadise/src/client.rs @@ -24,6 +24,9 @@ pub const DEFAULT_BLOCK_TIMEOUT_SECS: u64 = 180; /// Default User-Agent pub const DEFAULT_USER_AGENT: &str = "pmoparadise/0.1.0"; +/// Default channel (0 = main mix) +pub const DEFAULT_CHANNEL: u8 = 0; + /// Radio Paradise HTTP client /// /// This client provides access to Radio Paradise's streaming API, @@ -70,11 +73,14 @@ impl RadioParadiseClient { /// Create a client with a custom reqwest::Client /// /// Useful for sharing HTTP connection pools or custom proxy settings + /// + /// Note: Uses default settings (channel 0, default timeouts). + /// For more control, use `ClientBuilder::default().client(client).build()`. pub fn with_client(client: Client) -> Self { Self { client, api_base: DEFAULT_API_BASE.to_string(), - channel: 0, + channel: DEFAULT_CHANNEL, request_timeout: Duration::from_secs(DEFAULT_REQUEST_TIMEOUT_SECS), block_timeout: Duration::from_secs(DEFAULT_BLOCK_TIMEOUT_SECS), next_block_url: None, @@ -244,7 +250,7 @@ impl Default for ClientBuilder { Self { client: None, api_base: DEFAULT_API_BASE.to_string(), - channel: 0, + channel: DEFAULT_CHANNEL, request_timeout: Duration::from_secs(DEFAULT_REQUEST_TIMEOUT_SECS), block_timeout: Duration::from_secs(DEFAULT_BLOCK_TIMEOUT_SECS), user_agent: DEFAULT_USER_AGENT.to_string(), @@ -274,7 +280,6 @@ impl ClientBuilder { /// Set the channel (0 = main mix, 1 = mellow, 2 = rock, 3 = world/etc) pub fn channel(mut self, channel: u8) -> Self { self.channel = channel; - // block_base sera calculé dynamiquement dans build() à partir du channel self } @@ -339,6 +344,6 @@ mod tests { fn test_builder_defaults() { let builder = ClientBuilder::default(); assert_eq!(builder.api_base, DEFAULT_API_BASE); - assert_eq!(builder.channel, 0); + assert_eq!(builder.channel, DEFAULT_CHANNEL); } } From 98bf45cd279ba2a9220dc319a1eeb6deec3f9c3b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Nov 2025 11:21:33 +0000 Subject: [PATCH 4/4] feat: Add user-friendly default_channel configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute la possibilité de configurer le channel par défaut de Radio Paradise de manière persistante et user-friendly. Fonctionnalités : - get_paradise_default_channel() : récupère le channel configuré (défaut: 0/main) - set_paradise_default_channel(u8) : définit le channel par défaut - Accepte DEUX formats dans le fichier YAML : * Noms conviviaux : "main", "mellow", "rock", "eclectic" * IDs numériques : 0, 1, 2, 3 - Stocke les valeurs comme chaînes conviviales pour la lisibilité - Validation automatique avec fallback sur "main" si invalide - Persistence automatique de la valeur par défaut lors du premier accès Exemple de configuration YAML : ```yaml sources: radio_paradise: enabled: true default_channel: mellow # ou 1 ``` Cette amélioration rend la configuration plus accessible aux utilisateurs qui préfèrent un channel autre que Main Mix par défaut. --- pmoparadise/src/config_ext.rs | 111 ++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/pmoparadise/src/config_ext.rs b/pmoparadise/src/config_ext.rs index c2303595..49becbb5 100644 --- a/pmoparadise/src/config_ext.rs +++ b/pmoparadise/src/config_ext.rs @@ -21,6 +21,7 @@ //! } //! ``` +use crate::{channels::ParadiseChannelKind, client::DEFAULT_CHANNEL}; use anyhow::Result; use pmoconfig::Config; use serde_yaml::Value; @@ -81,6 +82,60 @@ pub trait RadioParadiseConfigExt { /// config.set_paradise_enabled(false)?; /// ``` fn set_paradise_enabled(&self, enabled: bool) -> Result<()>; + + /// Récupère le channel par défaut + /// + /// # Returns + /// + /// Le channel par défaut (0 = Main Mix par défaut). + /// + /// Si la valeur n'existe pas dans la configuration, elle est automatiquement + /// définie à "main" et persistée. + /// + /// # Channels disponibles + /// + /// Peut être configuré comme chaîne de caractères ou nombre : + /// - "main" ou 0 = Main Mix (eclectic, diverse mix) + /// - "mellow" ou 1 = Mellow Mix (smooth, chilled music) + /// - "rock" ou 2 = Rock Mix (classic & modern rock) + /// - "eclectic" ou 3 = Eclectic Mix (global sounds) + /// + /// # Exemple de configuration YAML + /// + /// ```yaml + /// sources: + /// radio_paradise: + /// default_channel: mellow # or 1 + /// ``` + /// + /// # Exemple d'utilisation + /// + /// ```rust,ignore + /// let channel = config.get_paradise_default_channel()?; + /// let client = RadioParadiseClient::builder().channel(channel).build().await?; + /// ``` + fn get_paradise_default_channel(&self) -> Result; + + /// Définit le channel par défaut + /// + /// # Arguments + /// + /// * `channel` - Le channel (0-3) + /// + /// La valeur est stockée sous forme de nom convivial ("main", "mellow", etc.) + /// dans le fichier de configuration. + /// + /// # Exemple + /// + /// ```rust,ignore + /// use pmoparadise::channels::ParadiseChannelKind; + /// + /// // Use Mellow Mix by default + /// config.set_paradise_default_channel(ParadiseChannelKind::Mellow.id())?; + /// // Or simply: + /// config.set_paradise_default_channel(1)?; + /// ``` + fn set_paradise_default_channel(&self, channel: u8) -> Result<()>; } impl RadioParadiseConfigExt for Config { @@ -101,6 +156,62 @@ impl RadioParadiseConfigExt for Config { Value::Bool(enabled), ) } + + fn get_paradise_default_channel(&self) -> Result { + match self.get_value(&["sources", "radio_paradise", "default_channel"]) { + Ok(Value::String(s)) => { + // Try to parse as channel name (e.g., "main", "mellow", etc.) + match s.parse::() { + Ok(kind) => Ok(kind.id()), + Err(_) => { + // Invalid channel name, use default + self.set_paradise_default_channel(DEFAULT_CHANNEL)?; + Ok(DEFAULT_CHANNEL) + } + } + } + Ok(Value::Number(n)) => { + // Accept numeric channel ID (0-3) + if let Some(ch) = n.as_u64() { + if ch <= 3 { + Ok(ch as u8) + } else { + // Invalid channel number, use default + self.set_paradise_default_channel(DEFAULT_CHANNEL)?; + Ok(DEFAULT_CHANNEL) + } + } else { + // Not a valid number, use default + self.set_paradise_default_channel(DEFAULT_CHANNEL)?; + Ok(DEFAULT_CHANNEL) + } + } + _ => { + // Use default and persist it as "main" (user-friendly) + self.set_value( + &["sources", "radio_paradise", "default_channel"], + Value::String("main".to_string()), + )?; + Ok(DEFAULT_CHANNEL) + } + } + } + + fn set_paradise_default_channel(&self, channel: u8) -> Result<()> { + // Convert channel ID to user-friendly string name + let channel_name = match channel { + 0 => "main", + 1 => "mellow", + 2 => "rock", + 3 => "eclectic", + _ => return Err(anyhow::anyhow!("Invalid channel ID: {}", channel)), + }; + + self.set_value( + &["sources", "radio_paradise", "default_channel"], + Value::String(channel_name.to_string()), + ) + } } #[cfg(test)]