From fe9dd9aba64a5aa4ad2791e9f2e91ea62c792ced Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sun, 5 Apr 2026 14:06:32 +0200 Subject: [PATCH] :recycle: refactor(pmowebrenderer): factoriser handlers et supprimer duplications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remplacer les Arc+Box::pin manuels par la macro action_handler!(captures(...)) - Supprimer helpers locaux dupliqués dans renderer.rs (add_var, add_action) - Extraire handlers génériques pour GET requests - Factoriser extraction metadata dans set_uri_handler /set_next_uriHandler - Simplifier build_renderingcontrol en retirant pipeline inutile - Mettre à jour edition Rust de 2021 vers 2024 dans tous les Cargo.toml - Corriger pattern matching inutile `ref` sur déréférencement dans pmoaudio et pmoflac --- .kilo/plans/1775386308232-quick-orchid.md | 108 ++++++ pmoapp/Cargo.toml | 2 +- pmoaudio-ext/Cargo.toml | 2 +- pmoaudio/Cargo.toml | 2 +- pmoaudio/src/audio_segment.rs | 4 +- pmoaudiocache/Cargo.toml | 2 +- pmocache/Cargo.toml | 2 +- pmoconfig/Cargo.toml | 2 +- pmocovers/Cargo.toml | 2 +- pmoflac/Cargo.toml | 2 +- pmoflac/src/encoder.rs | 2 +- pmoparadise/Cargo.toml | 2 +- pmoplaylist/Cargo.toml | 2 +- pmoqobuz/Cargo.toml | 2 +- pmoqobuz/src/source.rs | 2 +- pmoradiofrance/Cargo.toml | 2 +- pmosource/Cargo.toml | 2 +- pmoupnp/src/actions/action_handler.rs | 138 +++----- pmowebrenderer/Cargo.toml | 2 +- pmowebrenderer/src/handlers.rs | 391 ++++++++-------------- pmowebrenderer/src/renderer.rs | 40 +-- 21 files changed, 313 insertions(+), 400 deletions(-) create mode 100644 .kilo/plans/1775386308232-quick-orchid.md diff --git a/.kilo/plans/1775386308232-quick-orchid.md b/.kilo/plans/1775386308232-quick-orchid.md new file mode 100644 index 00000000..03f41d39 --- /dev/null +++ b/.kilo/plans/1775386308232-quick-orchid.md @@ -0,0 +1,108 @@ +# Plan: Suppression des duplications dans `@pmowebrenderer` + +## Objectif +Éliminer les redondances de code identifiées lors de l'audit. + +--- + +## Duplication 1: Helper functions dupliquées dans `renderer.rs` + +**Fichiers affectés**: `src/renderer.rs` + +**Problème**: +- Lignes 17-54: `add_arg_in`, `add_arg_out`, `add_var`, `add_action` définies +- Lignes 150-181: Dans `build_avtransport()`, réimplémentation locale avec closures `|svc, var| { ... }` +- Répétition de 20+ appels `add_var(&mut svc, &VAR)?` et `add_action(&mut svc, Arc::new(action))?` + +**Solution**: +1. Supprimer les closures locales redéclarées (lignes 150-181) +2. Utiliser directement les fonctions helpers du haut du fichier +3. Créer une macro ou fonction utilitaire pour les appels répétés: + ```rust + macro_rules! add_vars { + ($svc:expr, $($var:expr),*) => { $({ add_var($svc, &$var)?; })* }; + } + ``` + +--- + +## Duplication 2: Pattern handlers boilerplate dans `handlers.rs` + +**Fichiers affectés**: `src/handlers.rs` + +**Problème**: +- `play_handler`, `stop_handler`, `pause_handler` (lignes 23-71): structure identique +- `next_handler`, `previous_handler` (lignes 74-95): clones +- Handlers GET (lignes 166-317): pattern `state.clone()` + `Box::pin(async move { ... set!() ... })` dupliqué + +**Solution**: +1. Créer un helper générique: + ```rust + fn make_state_handler(state: SharedState, f: F) -> ActionHandler + where F: FnOnce(&mut ActionData, &RendererState) -> Result + Send + 'static + ``` +2. Factoriser les closures `let state = state.clone()` dans chaque handler + +--- + +## Duplication 3: Extraction metadata dupliquée + +**Fichiers affectés**: `src/handlers.rs` + +**Problème**: +- Lignes 118-123: `set_uri_handler` extraction metadata +- Lignes 146-151: `set_next_uri_handler` extraction metadata (identique) + +**Solution**: +1. Extraire en fonction utilitaire: + ```rust + fn extract_metadata(data: &ActionData, key: &str) -> String { ... } + ``` + +--- + +## Duplication 4: Méthodes pipeline dans `registry.rs` + +**Fichiers affectés**: `src/registry.rs` + +**Problème**: +- `send_pipeline_command` (lignes 272-279) appelle `get_pipeline` (lignes 281-283) +- `load_uri` (lignes 286-290), `send_play_command` (lignes 293-296), `send_pause_command` (lignes 299-301) sont des wrappers quasi-identiques + +**Solution**: +Consolider en méthodes génériques: +```rust +pub async fn send_command(&self, instance_id: &str, cmd: PipelineControl) { + if let Some(pipeline) = self.get_pipeline(instance_id) { + pipeline.send(cmd).await; + } +} +``` + +--- + +## Duplication 5: Feature flags avec code dupliqué + +**Fichiers affectés**: `src/registry.rs` + +**Problème**: +- Lignes 51-68 et 334-395: double impl de `create_instance` selon feature + +**Solution**: +- Extraire la logique commune dans une fonction privée +- Utiliser `#[cfg]` seulement pour les différences (appel à pmoserver) + +--- + +## Ordre de traitement suggéré + +1. **Phase 1**: Helpers dans `renderer.rs` (les plus simples) +2. **Phase 2**: Handlers dans `handlers.rs` (plus complexe, nécessite macro) +3. **Phase 3**: Méthodes pipeline dans `registry.rs` +4. **Phase 4**: Feature flags + +## Vérification +Après chaque phase, exécuter: +```bash +cargo check --package pmowebrenderer +``` \ No newline at end of file diff --git a/pmoapp/Cargo.toml b/pmoapp/Cargo.toml index b5c1e0cf..28c369e3 100644 --- a/pmoapp/Cargo.toml +++ b/pmoapp/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "pmoapp" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] rust-embed = "8.5.0" diff --git a/pmoaudio-ext/Cargo.toml b/pmoaudio-ext/Cargo.toml index debf6720..8612366a 100644 --- a/pmoaudio-ext/Cargo.toml +++ b/pmoaudio-ext/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "pmoaudio-ext" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] # Core audio types diff --git a/pmoaudio/Cargo.toml b/pmoaudio/Cargo.toml index d264e4d3..59857759 100755 --- a/pmoaudio/Cargo.toml +++ b/pmoaudio/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "pmoaudio" version = "0.1.0" -edition = "2021" +edition = "2024" [features] default = [] diff --git a/pmoaudio/src/audio_segment.rs b/pmoaudio/src/audio_segment.rs index 558776ae..c65729a6 100755 --- a/pmoaudio/src/audio_segment.rs +++ b/pmoaudio/src/audio_segment.rs @@ -422,7 +422,7 @@ impl<'a> TryInto<&'a Arc> for &'a AudioSegment { fn try_into(self) -> Result<&'a Arc, Self::Error> { match &self.segment { - _AudioSegment::Chunk(ref chunk) => Ok(chunk), + _AudioSegment::Chunk(chunk) => Ok(chunk), _ => Err(()), } } @@ -433,7 +433,7 @@ impl<'a> TryInto<&'a Arc> for &'a AudioSegment { fn try_into(self) -> Result<&'a Arc, Self::Error> { match &self.segment { - _AudioSegment::Sync(ref marker) => Ok(marker), + _AudioSegment::Sync(marker) => Ok(marker), _ => Err(()), } } diff --git a/pmoaudiocache/Cargo.toml b/pmoaudiocache/Cargo.toml index e3139df2..07e6cf84 100644 --- a/pmoaudiocache/Cargo.toml +++ b/pmoaudiocache/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "pmoaudiocache" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] # Cache générique diff --git a/pmocache/Cargo.toml b/pmocache/Cargo.toml index 1b4dd4b7..1626ab0f 100644 --- a/pmocache/Cargo.toml +++ b/pmocache/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "pmocache" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] # Base de données diff --git a/pmoconfig/Cargo.toml b/pmoconfig/Cargo.toml index 3a3790ad..5b81edf6 100644 --- a/pmoconfig/Cargo.toml +++ b/pmoconfig/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "pmoconfig" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] pmoutils = { version = "0.1.2", registry = "pmo" } diff --git a/pmocovers/Cargo.toml b/pmocovers/Cargo.toml index 9d1261e1..090199f0 100644 --- a/pmocovers/Cargo.toml +++ b/pmocovers/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "pmocovers" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] # Cache générique diff --git a/pmoflac/Cargo.toml b/pmoflac/Cargo.toml index c23460ea..b55b43e6 100755 --- a/pmoflac/Cargo.toml +++ b/pmoflac/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "pmoflac" version = "0.1.0" -edition = "2021" +edition = "2024" authors = ["PMOMusic"] description = "Asynchronous FLAC <-> PCM streaming utilities" license = "MIT" diff --git a/pmoflac/src/encoder.rs b/pmoflac/src/encoder.rs index a32decef..b1c35e57 100755 --- a/pmoflac/src/encoder.rs +++ b/pmoflac/src/encoder.rs @@ -467,7 +467,7 @@ unsafe fn setup_metadata( append_comment("TRACKNUMBER", &track_number.to_string())?; } // Construct cover URL: use cover_pk with server_base_url if available, fallback to cover_url - if let (Some(ref pk), Some(ref base_url)) = (&metadata.cover_pk, &metadata.server_base_url) { + if let (Some(pk), Some(base_url)) = (&metadata.cover_pk, &metadata.server_base_url) { let cover_url = format!("{}/covers/image/{}", base_url, pk); append_comment("COVERART", &cover_url)?; } else if let Some(cover_url) = &metadata.cover_url { diff --git a/pmoparadise/Cargo.toml b/pmoparadise/Cargo.toml index e5bad271..a6a1fb46 100644 --- a/pmoparadise/Cargo.toml +++ b/pmoparadise/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "pmoparadise" version = "0.1.0" -edition = "2021" +edition = "2024" authors = ["PMOMusic Contributors"] description = "Rust client for Radio Paradise streaming service" license = "MIT OR Apache-2.0" diff --git a/pmoplaylist/Cargo.toml b/pmoplaylist/Cargo.toml index b7146096..e026f6bc 100644 --- a/pmoplaylist/Cargo.toml +++ b/pmoplaylist/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "pmoplaylist" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] # Caches PMO diff --git a/pmoqobuz/Cargo.toml b/pmoqobuz/Cargo.toml index 3156cc03..cfd82d24 100644 --- a/pmoqobuz/Cargo.toml +++ b/pmoqobuz/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "pmoqobuz" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] regex = "1.12" diff --git a/pmoqobuz/src/source.rs b/pmoqobuz/src/source.rs index 685f69fe..905d70bd 100644 --- a/pmoqobuz/src/source.rs +++ b/pmoqobuz/src/source.rs @@ -252,7 +252,7 @@ impl QobuzSource { .await .ok(); - if let (Some(ref audio_pk), Some(ref cover_pk)) = (&cached_audio_pk, &cached_cover_pk) { + if let (Some(audio_pk), Some(cover_pk)) = (&cached_audio_pk, &cached_cover_pk) { let _ = self.inner .cache_manager diff --git a/pmoradiofrance/Cargo.toml b/pmoradiofrance/Cargo.toml index d8bef018..a37a02a8 100644 --- a/pmoradiofrance/Cargo.toml +++ b/pmoradiofrance/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "pmoradiofrance" version = "0.1.0" -edition = "2021" +edition = "2024" authors = ["PMOMusic Contributors"] description = "Rust client for Radio France streaming services" license = "MIT OR Apache-2.0" diff --git a/pmosource/Cargo.toml b/pmosource/Cargo.toml index 3d452f9f..0a4407ca 100644 --- a/pmosource/Cargo.toml +++ b/pmosource/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "pmosource" version = "0.1.0" -edition = "2021" +edition = "2024" authors = ["PMOMusic Contributors"] description = "Common traits and types for PMOMusic sources" license = "MIT OR Apache-2.0" diff --git a/pmoupnp/src/actions/action_handler.rs b/pmoupnp/src/actions/action_handler.rs index 7ef36045..c4017953 100644 --- a/pmoupnp/src/actions/action_handler.rs +++ b/pmoupnp/src/actions/action_handler.rs @@ -177,110 +177,27 @@ pub type ActionHandler = Arc ActionFuture + Send + Sync>; /// Macro pour créer facilement un ActionHandler. /// -/// Cette macro simplifie la création d'handlers asynchrones en cachant -/// la complexité de `Arc`, `Box::pin`, et `async move`. +/// Deux formes disponibles : /// -/// # Syntaxe +/// ## Forme simple (sans captures) /// /// ```ignore -/// action_handler!(|data| { -/// // votre logique async avec ActionData -/// // Modifier les données et les retourner -/// Ok(data) -/// }) +/// action_handler!(|data| { Ok(data) }) /// ``` /// -/// # Arguments +/// ## Forme avec captures (clonées automatiquement à chaque appel) /// -/// - `data` : Paramètre de type [`ActionData`] - HashMap contenant les valeurs des arguments -/// - Le corps du bloc peut contenir du code asynchrone (`.await`) -/// -/// # Type de retour -/// -/// La macro retourne un [`ActionHandler`] prêt à l'emploi. -/// -/// # Examples -/// -/// ## Exemple 1 : Handler simple (retourne les données telles quelles) +/// Pour capturer un état partagé ou un handle, utilisez `captures(...)`. +/// Chaque variable listée est clonée une fois par invocation du handler, +/// ce qui satisfait la contrainte `Fn` (et non `FnOnce`). /// /// ```ignore -/// use pmoupnp::action_handler; -/// -/// let handler = action_handler!(|data| { -/// Ok(data) // Retourne les données non modifiées -/// }); -/// ``` -/// -/// ## Exemple 2 : Handler qui calcule et modifie les données -/// -/// ```ignore -/// use pmoupnp::{action_handler, get, set}; -/// use pmoupnp::actions::ActionError; -/// -/// let handler = action_handler!(|mut data| { -/// // Extraire les valeurs avec la macro get! -/// let celsius: f64 = get!(data, "Celsius", f64); -/// -/// // Calculer -/// let fahrenheit = celsius * 9.0 / 5.0 + 32.0; -/// -/// // Insérer avec la macro set! -/// set!(data, "Fahrenheit", fahrenheit); -/// -/// Ok(data) // Retourner les données modifiées -/// }); -/// ``` -/// -/// ## Exemple 3 : Handler avec logique métier asynchrone -/// -/// ```ignore -/// use pmoupnp::{action_handler, get, set}; -/// use pmoupnp::actions::ActionError; -/// -/// let handler = action_handler!(|mut data| { -/// // Lire l'URI -/// let uri: String = get!(data, "URI", String); -/// -/// // Appel asynchrone à un service externe -/// let metadata = external_service::fetch_metadata(&uri).await -/// .map_err(|e| ActionError::ExternalError(e.to_string()))?; -/// -/// // Mettre à jour les données -/// set!(data, "Metadata", metadata); -/// -/// Ok(data) -/// }); -/// ``` -/// -/// ## Exemple 4 : Handler avec capture de contexte -/// -/// ```ignore -/// use pmoupnp::{action_handler, get, set}; -/// use pmoupnp::actions::ActionError; -/// use std::sync::Arc; -/// use tokio::sync::Mutex; -/// -/// // Contexte partagé -/// let player_state = Arc::new(Mutex::new(PlayerState::Stopped)); -/// -/// let handler = action_handler!(|mut data| { -/// // Vérifier l'état -/// { -/// let state = player_state.lock().await; -/// if *state == PlayerState::Error { -/// return Err(ActionError::InvalidState("Player in error state".into())); -/// } -/// } -/// -/// // Modifier l'état -/// { -/// let mut state = player_state.lock().await; -/// *state = PlayerState::Playing; -/// } -/// -/// // Mettre à jour les données -/// set!(data, "TransportState", "PLAYING".to_string()); +/// let state: SharedState = ...; +/// let pipeline: PipelineHandle = ...; /// +/// let handler = action_handler!(captures(state, pipeline) |mut data| { +/// pipeline.send(PipelineControl::Play).await; +/// state.write().playback_state = PlaybackState::Playing; /// Ok(data) /// }); /// ``` @@ -288,14 +205,41 @@ pub type ActionHandler = Arc ActionFuture + Send + Sync>; /// # Notes d'implémentation /// /// - Le bloc est automatiquement wrappé dans `async move` -/// - Les captures de variables sont déplacées (`move`) +/// - Avec `captures(...)`, chaque variable capturée doit implémenter `Clone` /// - Le résultat est automatiquement boxé et arcé -/// - Utilisez les macros `get!` et `set!` pour manipuler facilement les données #[macro_export] macro_rules! action_handler { + // ── Formes simples (sans captures externes) ────────────────────────────── + (|$data:ident| $body:block) => { std::sync::Arc::new(|$data: $crate::actions::ActionData| { Box::pin(async move $body) }) }; + + (|mut $data:ident| $body:block) => { + std::sync::Arc::new(|mut $data: $crate::actions::ActionData| { + Box::pin(async move $body) + }) + }; + + // ── Formes avec captures (clonées automatiquement à chaque appel) ──────── + // + // Chaque variable listée dans captures(...) est clonée avant chaque appel, + // ce qui satisfait la contrainte `Fn` (vs `FnOnce`). + // Les variables capturées doivent implémenter `Clone + Send + Sync + 'static`. + + (captures($($cap:ident),+ $(,)?) |$data:ident| $body:block) => { + std::sync::Arc::new(move |$data: $crate::actions::ActionData| { + $(let $cap = $cap.clone();)+ + Box::pin(async move $body) + }) + }; + + (captures($($cap:ident),+ $(,)?) |mut $data:ident| $body:block) => { + std::sync::Arc::new(move |mut $data: $crate::actions::ActionData| { + $(let $cap = $cap.clone();)+ + Box::pin(async move $body) + }) + }; } diff --git a/pmowebrenderer/Cargo.toml b/pmowebrenderer/Cargo.toml index c860a4a0..6661b080 100644 --- a/pmowebrenderer/Cargo.toml +++ b/pmowebrenderer/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "pmowebrenderer" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] pmoupnp = { path = "../pmoupnp" } diff --git a/pmowebrenderer/src/handlers.rs b/pmowebrenderer/src/handlers.rs index f0ac0616..be00caf8 100644 --- a/pmowebrenderer/src/handlers.rs +++ b/pmowebrenderer/src/handlers.rs @@ -3,316 +3,197 @@ //! Chaque handler bridge une action UPnP vers une commande `PipelineControl` //! envoyée au pipeline audio serveur, ou lit l'état partagé pour les requêtes GET. -use std::sync::Arc; - use pmodidl::DIDLLite; -use pmoupnp::actions::{ActionData, ActionError, ActionHandler, get_value}; -use pmoupnp::{get, set}; use pmodidl::ToXmlElement; +use pmoupnp::{action_handler, get, set}; +use pmoupnp::actions::{get_value, ActionHandler}; use crate::messages::PlaybackState; -use crate::pipeline::{PipelineControl, PipelineHandle, upnp_time_to_seconds}; +use crate::pipeline::{upnp_time_to_seconds, PipelineControl, PipelineHandle}; use crate::state::SharedState; -type ActionFuture = - std::pin::Pin> + Send>>; +// ─── AVTransport : commandes de transport ───────────────────────────────────── -// ─── AVTransport Handlers ─────────────────────────────────────────────────── - -/// Handler pour l'action UPnP "Play" - lance la lecture du flux audio pub fn play_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler { - Arc::new(move |data: ActionData| -> ActionFuture { - let pipeline = pipeline.clone(); - let state = state.clone(); - Box::pin(async move { - tracing::info!("[WebRenderer] UPnP Play action invoked"); - let has_uri = state.read().current_uri.is_some(); - { - let mut s = state.write(); - s.playback_state = PlaybackState::Transitioning; - } - if has_uri { - state.write().player_command = Some(serde_json::json!({ - "type": "stream", - "url": "/api/webrenderer/stream" - })); - tracing::info!("UPnP Play: stored stream command for frontend polling"); - } - pipeline.send(PipelineControl::Play).await; - Ok(data) - }) + action_handler!(captures(pipeline, state) |data| { + tracing::info!("[WebRenderer] UPnP Play action invoked"); + let has_uri = state.read().current_uri.is_some(); + state.write().playback_state = PlaybackState::Transitioning; + if has_uri { + state.write().player_command = Some(serde_json::json!({ + "type": "stream", + "url": "/api/webrenderer/stream" + })); + tracing::info!("UPnP Play: stored stream command for frontend polling"); + } + pipeline.send(PipelineControl::Play).await; + Ok(data) }) } -/// Handler pour l'action UPnP "Stop" - arrête la lecture pub fn stop_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler { - Arc::new(move |data: ActionData| -> ActionFuture { - let pipeline = pipeline.clone(); - let state = state.clone(); - Box::pin(async move { - pipeline.send(PipelineControl::Stop).await; - state.write().playback_state = PlaybackState::Stopped; - Ok(data) - }) + action_handler!(captures(pipeline, state) |data| { + pipeline.send(PipelineControl::Stop).await; + state.write().playback_state = PlaybackState::Stopped; + Ok(data) }) } -/// Handler pour l'action UPnP "Pause" - met en pause la lecture pub fn pause_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler { - Arc::new(move |data: ActionData| -> ActionFuture { - let pipeline = pipeline.clone(); - let state = state.clone(); - Box::pin(async move { - pipeline.send(PipelineControl::Pause).await; - state.write().playback_state = PlaybackState::Paused; - Ok(data) - }) + action_handler!(captures(pipeline, state) |data| { + pipeline.send(PipelineControl::Pause).await; + state.write().playback_state = PlaybackState::Paused; + Ok(data) }) } -/// Handler pour l'action UPnP "Next" - passe à la piste suivante pub fn next_handler(pipeline: PipelineHandle) -> ActionHandler { - let pipeline = pipeline.clone(); - Arc::new(move |data: ActionData| -> ActionFuture { - let pipeline = pipeline.clone(); - Box::pin(async move { - pipeline.send(PipelineControl::Play).await; - Ok(data) - }) + action_handler!(captures(pipeline) |data| { + pipeline.send(PipelineControl::Play).await; + Ok(data) }) } -/// Handler pour l'action UPnP "Previous" - retourne au début de la piste actuelle pub fn previous_handler(pipeline: PipelineHandle) -> ActionHandler { - let pipeline = pipeline.clone(); - Arc::new(move |data: ActionData| -> ActionFuture { - let pipeline = pipeline.clone(); - Box::pin(async move { - pipeline.send(PipelineControl::Play).await; - Ok(data) - }) + action_handler!(captures(pipeline) |data| { + pipeline.send(PipelineControl::Play).await; + Ok(data) }) } -/// Handler pour l'action UPnP "Seek" - seek à une position donnée pub fn seek_handler(pipeline: PipelineHandle) -> ActionHandler { - Arc::new(move |data: ActionData| -> ActionFuture { - let pipeline = pipeline.clone(); - Box::pin(async move { - let target: String = get!(&data, "Target", String); - let pos_sec = upnp_time_to_seconds(&target); - pipeline.send(PipelineControl::Seek(pos_sec)).await; - Ok(data) - }) + action_handler!(captures(pipeline) |data| { + let target: String = get!(&data, "Target", String); + let pos_sec = upnp_time_to_seconds(&target); + pipeline.send(PipelineControl::Seek(pos_sec)).await; + Ok(data) }) } -/// Handler pour l'action UPnP "SetAVTransportURI" - définit l'URI à jouer +// ─── AVTransport : chargement de média ──────────────────────────────────────── + pub fn set_uri_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler { - Arc::new(move |data: ActionData| -> ActionFuture { - let pipeline = pipeline.clone(); - let state = state.clone(); - Box::pin(async move { - tracing::info!("[WebRenderer] UPnP SetAVTransportURI action invoked"); - let uri: String = get!(&data, "CurrentURI", String); - let metadata: String = get_value::(&data, "CurrentURIMetaData") - .or_else(|_| { - get_value::(&data, "CurrentURIMetaData") - .map(|didl| didl.to_xml()) - }) - .unwrap_or_default(); + action_handler!(captures(pipeline, state) |mut data| { + tracing::info!("[WebRenderer] UPnP SetAVTransportURI action invoked"); + let uri: String = get!(&data, "CurrentURI", String); + let metadata: String = get_value::(&data, "CurrentURIMetaData") + .or_else(|_| get_value::(&data, "CurrentURIMetaData").map(|didl| didl.to_xml())) + .unwrap_or_default(); - tracing::info!(uri = %uri, "SetAVTransportURI handler called - loading URI into pipeline"); - pipeline.send(PipelineControl::LoadUri(uri.clone())).await; + tracing::info!(uri = %uri, "SetAVTransportURI handler called - loading URI into pipeline"); + pipeline.send(PipelineControl::LoadUri(uri.clone())).await; - { - let mut s = state.write(); - s.current_uri = Some(uri); - s.current_metadata = Some(metadata); - s.playback_state = PlaybackState::Transitioning; - } - Ok(data) - }) + { + let mut s = state.write(); + s.current_uri = Some(uri); + s.current_metadata = Some(metadata); + s.playback_state = PlaybackState::Transitioning; + } + Ok(data) }) } -/// Handler pour l'action UPnP "SetNextAVTransportURI" - définit l'URI suivante (gapless) pub fn set_next_uri_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler { - Arc::new(move |data: ActionData| -> ActionFuture { - let pipeline = pipeline.clone(); - let state = state.clone(); - Box::pin(async move { - let uri: String = get!(&data, "NextURI", String); - let metadata: String = get_value::(&data, "NextURIMetaData") - .or_else(|_| { - get_value::(&data, "NextURIMetaData") - .map(|didl| didl.to_xml()) - }) - .unwrap_or_default(); + action_handler!(captures(pipeline, state) |mut data| { + let uri: String = get!(&data, "NextURI", String); + let metadata: String = get_value::(&data, "NextURIMetaData") + .or_else(|_| get_value::(&data, "NextURIMetaData").map(|didl| didl.to_xml())) + .unwrap_or_default(); - pipeline.send(PipelineControl::LoadNextUri(uri.clone())).await; - - { - let mut s = state.write(); - s.next_uri = Some(uri); - s.next_metadata = Some(metadata); - } - Ok(data) - }) + pipeline.send(PipelineControl::LoadNextUri(uri.clone())).await; + { + let mut s = state.write(); + s.next_uri = Some(uri); + s.next_metadata = Some(metadata); + } + Ok(data) }) } -/// Handler pour l'action UPnP "GetPositionInfo" - retourne la position actuelle +// ─── AVTransport : getters ───────────────────────────────────────────────────── + pub fn get_position_info_handler(state: SharedState) -> ActionHandler { - Arc::new(move |data: ActionData| -> ActionFuture { - let state = state.clone(); - Box::pin(async move { - let mut data = data; - let s = state.read(); - set!( - &mut data, - "Track", - if s.current_uri.is_some() { 1u32 } else { 0u32 } - ); - set!( - &mut data, - "TrackDuration", - s.duration.clone().unwrap_or_else(|| "00:00:00".to_string()) - ); - set!( - &mut data, - "TrackURI", - s.current_uri.clone().unwrap_or_default() - ); - set!( - &mut data, - "TrackMetaData", - s.current_metadata.clone().unwrap_or_default() - ); - set!( - &mut data, - "RelTime", - s.position.clone().unwrap_or_else(|| "00:00:00".to_string()) - ); - set!( - &mut data, - "AbsTime", - s.position.clone().unwrap_or_else(|| "00:00:00".to_string()) - ); - Ok(data) - }) + action_handler!(captures(state) |mut data| { + let s = state.read(); + set!(&mut data, "Track", if s.current_uri.is_some() { 1u32 } else { 0u32 }); + set!(&mut data, "TrackDuration", s.duration.clone().unwrap_or_else(|| "00:00:00".to_string())); + set!(&mut data, "TrackURI", s.current_uri.clone().unwrap_or_default()); + set!(&mut data, "TrackMetaData", s.current_metadata.clone().unwrap_or_default()); + set!(&mut data, "RelTime", s.position.clone().unwrap_or_else(|| "00:00:00".to_string())); + set!(&mut data, "AbsTime", s.position.clone().unwrap_or_else(|| "00:00:00".to_string())); + Ok(data) }) } -/// Handler pour l'action UPnP "GetTransportInfo" - retourne l'état du transport pub fn get_transport_info_handler(state: SharedState) -> ActionHandler { - Arc::new(move |data: ActionData| -> ActionFuture { - let state = state.clone(); - Box::pin(async move { - let mut data = data; - let s = state.read(); - tracing::info!("[WebRenderer] GetTransportInfo: state={:?}", s.playback_state); - let transport_state = match s.playback_state { - PlaybackState::Stopped => "STOPPED", - PlaybackState::Playing => "PLAYING", - PlaybackState::Paused => "PAUSED_PLAYBACK", - PlaybackState::Transitioning => "TRANSITIONING", - }; - set!(&mut data, "CurrentTransportState", transport_state.to_string()); - set!(&mut data, "CurrentTransportStatus", "OK".to_string()); - set!(&mut data, "CurrentSpeed", "1".to_string()); - Ok(data) - }) + action_handler!(captures(state) |mut data| { + let s = state.read(); + tracing::info!("[WebRenderer] GetTransportInfo: state={:?}", s.playback_state); + let transport_state = match s.playback_state { + PlaybackState::Stopped => "STOPPED", + PlaybackState::Playing => "PLAYING", + PlaybackState::Paused => "PAUSED_PLAYBACK", + PlaybackState::Transitioning => "TRANSITIONING", + }; + set!(&mut data, "CurrentTransportState", transport_state.to_string()); + set!(&mut data, "CurrentTransportStatus", "OK".to_string()); + set!(&mut data, "CurrentSpeed", "1".to_string()); + Ok(data) }) } -/// Handler pour l'action UPnP "GetMediaInfo" - retourne les infos du média pub fn get_media_info_handler(state: SharedState) -> ActionHandler { - Arc::new(move |data: ActionData| -> ActionFuture { - let state = state.clone(); - Box::pin(async move { - let mut data = data; - let s = state.read(); - set!( - &mut data, - "NrTracks", - if s.current_uri.is_some() { 1u32 } else { 0u32 } - ); - set!(&mut data, "CurrentURI", s.current_uri.clone().unwrap_or_default()); - set!(&mut data, "CurrentURIMetaData", s.current_metadata.clone().unwrap_or_default()); - set!(&mut data, "NextURI", s.next_uri.clone().unwrap_or_default()); - set!(&mut data, "NextURIMetaData", s.next_metadata.clone().unwrap_or_default()); - Ok(data) - }) + action_handler!(captures(state) |mut data| { + let s = state.read(); + set!(&mut data, "NrTracks", if s.current_uri.is_some() { 1u32 } else { 0u32 }); + set!(&mut data, "CurrentURI", s.current_uri.clone().unwrap_or_default()); + set!(&mut data, "CurrentURIMetaData", s.current_metadata.clone().unwrap_or_default()); + set!(&mut data, "NextURI", s.next_uri.clone().unwrap_or_default()); + set!(&mut data, "NextURIMetaData", s.next_metadata.clone().unwrap_or_default()); + Ok(data) }) } -// ─── RenderingControl Handlers ────────────────────────────────────────────── +// ─── ConnectionManager ───────────────────────────────────────────────────────── -/// Handler pour l'action UPnP "SetVolume" - définit le volume -pub fn set_volume_handler(_pipeline: PipelineHandle, state: SharedState) -> ActionHandler { - Arc::new(move |data: ActionData| -> ActionFuture { - let state = state.clone(); - Box::pin(async move { - let volume: u16 = get!(&data, "DesiredVolume", u16); - state.write().volume = volume; - Ok(data) - }) - }) -} - -/// Handler pour l'action UPnP "GetVolume" - retourne le volume actuel -pub fn get_volume_handler(state: SharedState) -> ActionHandler { - Arc::new(move |data: ActionData| -> ActionFuture { - let state = state.clone(); - Box::pin(async move { - let mut data = data; - let volume = state.read().volume; - set!(&mut data, "CurrentVolume", volume); - Ok(data) - }) - }) -} - -/// Handler pour l'action UPnP "SetMute" - définit le mute -pub fn set_mute_handler(_pipeline: PipelineHandle, state: SharedState) -> ActionHandler { - Arc::new(move |data: ActionData| -> ActionFuture { - let state = state.clone(); - Box::pin(async move { - let mute: bool = get!(&data, "DesiredMute", bool); - state.write().mute = mute; - Ok(data) - }) - }) -} - -/// Handler pour l'action UPnP "GetMute" - retourne l'état mute -pub fn get_mute_handler(state: SharedState) -> ActionHandler { - Arc::new(move |data: ActionData| -> ActionFuture { - let state = state.clone(); - Box::pin(async move { - let mut data = data; - let mute = state.read().mute; - set!(&mut data, "CurrentMute", mute); - Ok(data) - }) - }) -} - -// ─── ConnectionManager Handlers ───────────────────────────────────────────── - -/// Handler pour l'action UPnP "GetProtocolInfo" - retourne les protocoles supportés pub fn get_protocol_info_handler() -> ActionHandler { - Arc::new(move |data: ActionData| -> ActionFuture { - Box::pin(async move { - let mut data = data; - set!(&mut data, "Source", String::new()); - set!( - &mut data, - "Sink", - "http-get:*:audio/flac:*,http-get:*:audio/x-flac:*".to_string() - ); - Ok(data) - }) + action_handler!(|mut data| { + set!(&mut data, "Source", String::new()); + set!(&mut data, "Sink", "http-get:*:audio/flac:*,http-get:*:audio/x-flac:*".to_string()); + Ok(data) + }) +} + +// ─── RenderingControl ────────────────────────────────────────────────────────── + +pub fn set_volume_handler(state: SharedState) -> ActionHandler { + action_handler!(captures(state) |mut data| { + let volume: u16 = get!(&data, "DesiredVolume", u16); + state.write().volume = volume; + Ok(data) + }) +} + +pub fn get_volume_handler(state: SharedState) -> ActionHandler { + action_handler!(captures(state) |mut data| { + let volume = state.read().volume; + set!(&mut data, "CurrentVolume", volume); + Ok(data) + }) +} + +pub fn set_mute_handler(state: SharedState) -> ActionHandler { + action_handler!(captures(state) |mut data| { + let mute: bool = get!(&data, "DesiredMute", bool); + state.write().mute = mute; + Ok(data) + }) +} + +pub fn get_mute_handler(state: SharedState) -> ActionHandler { + action_handler!(captures(state) |mut data| { + let mute = state.read().mute; + set!(&mut data, "CurrentMute", mute); + Ok(data) }) } diff --git a/pmowebrenderer/src/renderer.rs b/pmowebrenderer/src/renderer.rs index 13a6cce5..9291f520 100644 --- a/pmowebrenderer/src/renderer.rs +++ b/pmowebrenderer/src/renderer.rs @@ -118,7 +118,7 @@ impl WebRendererFactory { state: SharedState, ) -> Result { let avtransport = Self::build_avtransport(pipeline.clone(), state.clone())?; - let renderingcontrol = Self::build_renderingcontrol(pipeline.clone(), state.clone())?; + let renderingcontrol = Self::build_renderingcontrol(state.clone())?; let connectionmanager = Self::build_connectionmanager()?; let short_name = extract_browser_name(browser_ua); @@ -147,10 +147,6 @@ impl WebRendererFactory { state: SharedState, ) -> Result { let mut svc = Service::new("AVTransport".to_string()); - let add_var = |svc: &mut Service, var: &Arc| { - svc.add_variable(Arc::clone(var)) - .map_err(|e| FactoryError::VariableError(e.to_string())) - }; // Ajouter toutes les variables d'état add_var(&mut svc, &AVT_INSTANCE_ID)?; @@ -176,11 +172,6 @@ impl WebRendererFactory { add_var(&mut svc, &TRANSPORTSTATE)?; add_var(&mut svc, &TRANSPORTSTATUS)?; - let add_action = |svc: &mut Service, action: Arc| { - svc.add_action(action) - .map_err(|e| FactoryError::ActionError(e.to_string())) - }; - // Play let mut play = Action::new("Play".to_string()); add_arg_in(&mut play, "InstanceID", &AVT_INSTANCE_ID)?; @@ -304,31 +295,21 @@ impl WebRendererFactory { Ok(svc) } - /// Construit le service RenderingControl avec les handlers pipeline - fn build_renderingcontrol( - pipeline: PipelineHandle, - state: SharedState, - ) -> Result { + /// Construit le service RenderingControl + fn build_renderingcontrol(state: SharedState) -> Result { let mut svc = Service::new("RenderingControl".to_string()); - svc.add_variable(Arc::clone(&RC_INSTANCE_ID)) - .map_err(|e| FactoryError::VariableError(e.to_string()))?; - svc.add_variable(Arc::clone(&A_ARG_TYPE_CHANNEL)) - .map_err(|e| FactoryError::VariableError(e.to_string()))?; - svc.add_variable(Arc::clone(&VOLUME)) - .map_err(|e| FactoryError::VariableError(e.to_string()))?; - svc.add_variable(Arc::clone(&MUTE)) - .map_err(|e| FactoryError::VariableError(e.to_string()))?; + add_var(&mut svc, &RC_INSTANCE_ID)?; + add_var(&mut svc, &A_ARG_TYPE_CHANNEL)?; + add_var(&mut svc, &VOLUME)?; + add_var(&mut svc, &MUTE)?; // SetVolume let mut set_vol = Action::new("SetVolume".to_string()); add_arg_in(&mut set_vol, "InstanceID", &RC_INSTANCE_ID)?; add_arg_in(&mut set_vol, "Channel", &A_ARG_TYPE_CHANNEL)?; add_arg_in(&mut set_vol, "DesiredVolume", &VOLUME)?; - set_vol.set_handler(handlers::set_volume_handler( - pipeline.clone(), - state.clone(), - )); + set_vol.set_handler(handlers::set_volume_handler(state.clone())); add_action(&mut svc, Arc::new(set_vol))?; // GetVolume @@ -345,7 +326,7 @@ impl WebRendererFactory { add_arg_in(&mut set_mute, "InstanceID", &RC_INSTANCE_ID)?; add_arg_in(&mut set_mute, "Channel", &A_ARG_TYPE_CHANNEL)?; add_arg_in(&mut set_mute, "DesiredMute", &MUTE)?; - set_mute.set_handler(handlers::set_mute_handler(pipeline.clone(), state.clone())); + set_mute.set_handler(handlers::set_mute_handler(state.clone())); add_action(&mut svc, Arc::new(set_mute))?; // GetMute @@ -355,8 +336,7 @@ impl WebRendererFactory { add_arg_out(&mut get_mute, "CurrentMute", &MUTE)?; get_mute.set_stateful(false); get_mute.set_handler(handlers::get_mute_handler(state.clone())); - svc.add_action(Arc::new(get_mute)) - .map_err(|e| FactoryError::ActionError(format!("{:?}", e)))?; + add_action(&mut svc, Arc::new(get_mute))?; Ok(svc) }