♻️ refactor(pmowebrenderer): factoriser handlers et supprimer duplications

- 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
This commit is contained in:
2026-04-05 14:06:32 +02:00
parent 512dcd1803
commit fe9dd9aba6
21 changed files with 313 additions and 400 deletions

View File

@@ -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<F>(state: SharedState, f: F) -> ActionHandler
where F: FnOnce(&mut ActionData, &RendererState) -> Result<ActionData, ActionError> + 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
```

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "pmoapp" name = "pmoapp"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
rust-embed = "8.5.0" rust-embed = "8.5.0"

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "pmoaudio-ext" name = "pmoaudio-ext"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
# Core audio types # Core audio types

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "pmoaudio" name = "pmoaudio"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[features] [features]
default = [] default = []

View File

@@ -422,7 +422,7 @@ impl<'a> TryInto<&'a Arc<AudioChunk>> for &'a AudioSegment {
fn try_into(self) -> Result<&'a Arc<AudioChunk>, Self::Error> { fn try_into(self) -> Result<&'a Arc<AudioChunk>, Self::Error> {
match &self.segment { match &self.segment {
_AudioSegment::Chunk(ref chunk) => Ok(chunk), _AudioSegment::Chunk(chunk) => Ok(chunk),
_ => Err(()), _ => Err(()),
} }
} }
@@ -433,7 +433,7 @@ impl<'a> TryInto<&'a Arc<SyncMarker>> for &'a AudioSegment {
fn try_into(self) -> Result<&'a Arc<SyncMarker>, Self::Error> { fn try_into(self) -> Result<&'a Arc<SyncMarker>, Self::Error> {
match &self.segment { match &self.segment {
_AudioSegment::Sync(ref marker) => Ok(marker), _AudioSegment::Sync(marker) => Ok(marker),
_ => Err(()), _ => Err(()),
} }
} }

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "pmoaudiocache" name = "pmoaudiocache"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
# Cache générique # Cache générique

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "pmocache" name = "pmocache"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
# Base de données # Base de données

View File

@@ -2,7 +2,7 @@
[package] [package]
name = "pmoconfig" name = "pmoconfig"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
pmoutils = { version = "0.1.2", registry = "pmo" } pmoutils = { version = "0.1.2", registry = "pmo" }

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "pmocovers" name = "pmocovers"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
# Cache générique # Cache générique

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "pmoflac" name = "pmoflac"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
authors = ["PMOMusic"] authors = ["PMOMusic"]
description = "Asynchronous FLAC <-> PCM streaming utilities" description = "Asynchronous FLAC <-> PCM streaming utilities"
license = "MIT" license = "MIT"

View File

@@ -467,7 +467,7 @@ unsafe fn setup_metadata(
append_comment("TRACKNUMBER", &track_number.to_string())?; append_comment("TRACKNUMBER", &track_number.to_string())?;
} }
// Construct cover URL: use cover_pk with server_base_url if available, fallback to cover_url // 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); let cover_url = format!("{}/covers/image/{}", base_url, pk);
append_comment("COVERART", &cover_url)?; append_comment("COVERART", &cover_url)?;
} else if let Some(cover_url) = &metadata.cover_url { } else if let Some(cover_url) = &metadata.cover_url {

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "pmoparadise" name = "pmoparadise"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
authors = ["PMOMusic Contributors"] authors = ["PMOMusic Contributors"]
description = "Rust client for Radio Paradise streaming service" description = "Rust client for Radio Paradise streaming service"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "pmoplaylist" name = "pmoplaylist"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
# Caches PMO # Caches PMO

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "pmoqobuz" name = "pmoqobuz"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
regex = "1.12" regex = "1.12"

View File

@@ -252,7 +252,7 @@ impl QobuzSource {
.await .await
.ok(); .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 _ = let _ =
self.inner self.inner
.cache_manager .cache_manager

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "pmoradiofrance" name = "pmoradiofrance"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
authors = ["PMOMusic Contributors"] authors = ["PMOMusic Contributors"]
description = "Rust client for Radio France streaming services" description = "Rust client for Radio France streaming services"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "pmosource" name = "pmosource"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
authors = ["PMOMusic Contributors"] authors = ["PMOMusic Contributors"]
description = "Common traits and types for PMOMusic sources" description = "Common traits and types for PMOMusic sources"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"

View File

@@ -177,110 +177,27 @@ pub type ActionHandler = Arc<dyn Fn(ActionData) -> ActionFuture + Send + Sync>;
/// Macro pour créer facilement un ActionHandler. /// Macro pour créer facilement un ActionHandler.
/// ///
/// Cette macro simplifie la création d'handlers asynchrones en cachant /// Deux formes disponibles :
/// la complexité de `Arc`, `Box::pin`, et `async move`.
/// ///
/// # Syntaxe /// ## Forme simple (sans captures)
/// ///
/// ```ignore /// ```ignore
/// action_handler!(|data| { /// action_handler!(|data| { Ok(data) })
/// // votre logique async avec ActionData
/// // Modifier les données et les retourner
/// Ok(data)
/// })
/// ``` /// ```
/// ///
/// # Arguments /// ## Forme avec captures (clonées automatiquement à chaque appel)
/// ///
/// - `data` : Paramètre de type [`ActionData`] - HashMap contenant les valeurs des arguments /// Pour capturer un état partagé ou un handle, utilisez `captures(...)`.
/// - Le corps du bloc peut contenir du code asynchrone (`.await`) /// Chaque variable listée est clonée une fois par invocation du handler,
/// /// ce qui satisfait la contrainte `Fn` (et non `FnOnce`).
/// # Type de retour
///
/// La macro retourne un [`ActionHandler`] prêt à l'emploi.
///
/// # Examples
///
/// ## Exemple 1 : Handler simple (retourne les données telles quelles)
/// ///
/// ```ignore /// ```ignore
/// use pmoupnp::action_handler; /// let state: SharedState = ...;
/// /// let pipeline: PipelineHandle = ...;
/// 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 handler = action_handler!(captures(state, pipeline) |mut data| {
/// pipeline.send(PipelineControl::Play).await;
/// state.write().playback_state = PlaybackState::Playing;
/// Ok(data) /// Ok(data)
/// }); /// });
/// ``` /// ```
@@ -288,14 +205,41 @@ pub type ActionHandler = Arc<dyn Fn(ActionData) -> ActionFuture + Send + Sync>;
/// # Notes d'implémentation /// # Notes d'implémentation
/// ///
/// - Le bloc est automatiquement wrappé dans `async move` /// - 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é /// - Le résultat est automatiquement boxé et arcé
/// - Utilisez les macros `get!` et `set!` pour manipuler facilement les données
#[macro_export] #[macro_export]
macro_rules! action_handler { macro_rules! action_handler {
// ── Formes simples (sans captures externes) ──────────────────────────────
(|$data:ident| $body:block) => { (|$data:ident| $body:block) => {
std::sync::Arc::new(|$data: $crate::actions::ActionData| { std::sync::Arc::new(|$data: $crate::actions::ActionData| {
Box::pin(async move $body) 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)
})
};
} }

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "pmowebrenderer" name = "pmowebrenderer"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
pmoupnp = { path = "../pmoupnp" } pmoupnp = { path = "../pmoupnp" }

View File

@@ -3,316 +3,197 @@
//! Chaque handler bridge une action UPnP vers une commande `PipelineControl` //! 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. //! envoyée au pipeline audio serveur, ou lit l'état partagé pour les requêtes GET.
use std::sync::Arc;
use pmodidl::DIDLLite; use pmodidl::DIDLLite;
use pmoupnp::actions::{ActionData, ActionError, ActionHandler, get_value};
use pmoupnp::{get, set};
use pmodidl::ToXmlElement; use pmodidl::ToXmlElement;
use pmoupnp::{action_handler, get, set};
use pmoupnp::actions::{get_value, ActionHandler};
use crate::messages::PlaybackState; 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; use crate::state::SharedState;
type ActionFuture = // ─── AVTransport : commandes de transport ─────────────────────────────────────
std::pin::Pin<Box<dyn std::future::Future<Output = Result<ActionData, ActionError>> + Send>>;
// ─── AVTransport Handlers ───────────────────────────────────────────────────
/// Handler pour l'action UPnP "Play" - lance la lecture du flux audio
pub fn play_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler { pub fn play_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture { action_handler!(captures(pipeline, state) |data| {
let pipeline = pipeline.clone(); tracing::info!("[WebRenderer] UPnP Play action invoked");
let state = state.clone(); let has_uri = state.read().current_uri.is_some();
Box::pin(async move { state.write().playback_state = PlaybackState::Transitioning;
tracing::info!("[WebRenderer] UPnP Play action invoked"); if has_uri {
let has_uri = state.read().current_uri.is_some(); state.write().player_command = Some(serde_json::json!({
{ "type": "stream",
let mut s = state.write(); "url": "/api/webrenderer/stream"
s.playback_state = PlaybackState::Transitioning; }));
} tracing::info!("UPnP Play: stored stream command for frontend polling");
if has_uri { }
state.write().player_command = Some(serde_json::json!({ pipeline.send(PipelineControl::Play).await;
"type": "stream", Ok(data)
"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 { pub fn stop_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture { action_handler!(captures(pipeline, state) |data| {
let pipeline = pipeline.clone(); pipeline.send(PipelineControl::Stop).await;
let state = state.clone(); state.write().playback_state = PlaybackState::Stopped;
Box::pin(async move { Ok(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 { pub fn pause_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture { action_handler!(captures(pipeline, state) |data| {
let pipeline = pipeline.clone(); pipeline.send(PipelineControl::Pause).await;
let state = state.clone(); state.write().playback_state = PlaybackState::Paused;
Box::pin(async move { Ok(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 { pub fn next_handler(pipeline: PipelineHandle) -> ActionHandler {
let pipeline = pipeline.clone(); action_handler!(captures(pipeline) |data| {
Arc::new(move |data: ActionData| -> ActionFuture { pipeline.send(PipelineControl::Play).await;
let pipeline = pipeline.clone(); Ok(data)
Box::pin(async move {
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 { pub fn previous_handler(pipeline: PipelineHandle) -> ActionHandler {
let pipeline = pipeline.clone(); action_handler!(captures(pipeline) |data| {
Arc::new(move |data: ActionData| -> ActionFuture { pipeline.send(PipelineControl::Play).await;
let pipeline = pipeline.clone(); Ok(data)
Box::pin(async move {
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 { pub fn seek_handler(pipeline: PipelineHandle) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture { action_handler!(captures(pipeline) |data| {
let pipeline = pipeline.clone(); let target: String = get!(&data, "Target", String);
Box::pin(async move { let pos_sec = upnp_time_to_seconds(&target);
let target: String = get!(&data, "Target", String); pipeline.send(PipelineControl::Seek(pos_sec)).await;
let pos_sec = upnp_time_to_seconds(&target); Ok(data)
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 { pub fn set_uri_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture { action_handler!(captures(pipeline, state) |mut data| {
let pipeline = pipeline.clone(); tracing::info!("[WebRenderer] UPnP SetAVTransportURI action invoked");
let state = state.clone(); let uri: String = get!(&data, "CurrentURI", String);
Box::pin(async move { let metadata: String = get_value::<String>(&data, "CurrentURIMetaData")
tracing::info!("[WebRenderer] UPnP SetAVTransportURI action invoked"); .or_else(|_| get_value::<DIDLLite>(&data, "CurrentURIMetaData").map(|didl| didl.to_xml()))
let uri: String = get!(&data, "CurrentURI", String); .unwrap_or_default();
let metadata: String = get_value::<String>(&data, "CurrentURIMetaData")
.or_else(|_| {
get_value::<DIDLLite>(&data, "CurrentURIMetaData")
.map(|didl| didl.to_xml())
})
.unwrap_or_default();
tracing::info!(uri = %uri, "SetAVTransportURI handler called - loading URI into pipeline"); tracing::info!(uri = %uri, "SetAVTransportURI handler called - loading URI into pipeline");
pipeline.send(PipelineControl::LoadUri(uri.clone())).await; pipeline.send(PipelineControl::LoadUri(uri.clone())).await;
{ {
let mut s = state.write(); let mut s = state.write();
s.current_uri = Some(uri); s.current_uri = Some(uri);
s.current_metadata = Some(metadata); s.current_metadata = Some(metadata);
s.playback_state = PlaybackState::Transitioning; s.playback_state = PlaybackState::Transitioning;
} }
Ok(data) 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 { pub fn set_next_uri_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture { action_handler!(captures(pipeline, state) |mut data| {
let pipeline = pipeline.clone(); let uri: String = get!(&data, "NextURI", String);
let state = state.clone(); let metadata: String = get_value::<String>(&data, "NextURIMetaData")
Box::pin(async move { .or_else(|_| get_value::<DIDLLite>(&data, "NextURIMetaData").map(|didl| didl.to_xml()))
let uri: String = get!(&data, "NextURI", String); .unwrap_or_default();
let metadata: String = get_value::<String>(&data, "NextURIMetaData")
.or_else(|_| {
get_value::<DIDLLite>(&data, "NextURIMetaData")
.map(|didl| didl.to_xml())
})
.unwrap_or_default();
pipeline.send(PipelineControl::LoadNextUri(uri.clone())).await; pipeline.send(PipelineControl::LoadNextUri(uri.clone())).await;
{
{ let mut s = state.write();
let mut s = state.write(); s.next_uri = Some(uri);
s.next_uri = Some(uri); s.next_metadata = Some(metadata);
s.next_metadata = Some(metadata); }
} Ok(data)
Ok(data)
})
}) })
} }
/// Handler pour l'action UPnP "GetPositionInfo" - retourne la position actuelle // ─── AVTransport : getters ─────────────────────────────────────────────────────
pub fn get_position_info_handler(state: SharedState) -> ActionHandler { pub fn get_position_info_handler(state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture { action_handler!(captures(state) |mut data| {
let state = state.clone(); let s = state.read();
Box::pin(async move { set!(&mut data, "Track", if s.current_uri.is_some() { 1u32 } else { 0u32 });
let mut data = data; set!(&mut data, "TrackDuration", s.duration.clone().unwrap_or_else(|| "00:00:00".to_string()));
let s = state.read(); set!(&mut data, "TrackURI", s.current_uri.clone().unwrap_or_default());
set!( set!(&mut data, "TrackMetaData", s.current_metadata.clone().unwrap_or_default());
&mut data, set!(&mut data, "RelTime", s.position.clone().unwrap_or_else(|| "00:00:00".to_string()));
"Track", set!(&mut data, "AbsTime", s.position.clone().unwrap_or_else(|| "00:00:00".to_string()));
if s.current_uri.is_some() { 1u32 } else { 0u32 } Ok(data)
);
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 { pub fn get_transport_info_handler(state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture { action_handler!(captures(state) |mut data| {
let state = state.clone(); let s = state.read();
Box::pin(async move { tracing::info!("[WebRenderer] GetTransportInfo: state={:?}", s.playback_state);
let mut data = data; let transport_state = match s.playback_state {
let s = state.read(); PlaybackState::Stopped => "STOPPED",
tracing::info!("[WebRenderer] GetTransportInfo: state={:?}", s.playback_state); PlaybackState::Playing => "PLAYING",
let transport_state = match s.playback_state { PlaybackState::Paused => "PAUSED_PLAYBACK",
PlaybackState::Stopped => "STOPPED", PlaybackState::Transitioning => "TRANSITIONING",
PlaybackState::Playing => "PLAYING", };
PlaybackState::Paused => "PAUSED_PLAYBACK", set!(&mut data, "CurrentTransportState", transport_state.to_string());
PlaybackState::Transitioning => "TRANSITIONING", set!(&mut data, "CurrentTransportStatus", "OK".to_string());
}; set!(&mut data, "CurrentSpeed", "1".to_string());
set!(&mut data, "CurrentTransportState", transport_state.to_string()); Ok(data)
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 { pub fn get_media_info_handler(state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture { action_handler!(captures(state) |mut data| {
let state = state.clone(); let s = state.read();
Box::pin(async move { set!(&mut data, "NrTracks", if s.current_uri.is_some() { 1u32 } else { 0u32 });
let mut data = data; set!(&mut data, "CurrentURI", s.current_uri.clone().unwrap_or_default());
let s = state.read(); set!(&mut data, "CurrentURIMetaData", s.current_metadata.clone().unwrap_or_default());
set!( set!(&mut data, "NextURI", s.next_uri.clone().unwrap_or_default());
&mut data, set!(&mut data, "NextURIMetaData", s.next_metadata.clone().unwrap_or_default());
"NrTracks", Ok(data)
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 { pub fn get_protocol_info_handler() -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture { action_handler!(|mut data| {
Box::pin(async move { set!(&mut data, "Source", String::new());
let mut data = data; set!(&mut data, "Sink", "http-get:*:audio/flac:*,http-get:*:audio/x-flac:*".to_string());
set!(&mut data, "Source", String::new()); Ok(data)
set!( })
&mut data, }
"Sink",
"http-get:*:audio/flac:*,http-get:*:audio/x-flac:*".to_string() // ─── RenderingControl ──────────────────────────────────────────────────────────
);
Ok(data) 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)
}) })
} }

View File

@@ -118,7 +118,7 @@ impl WebRendererFactory {
state: SharedState, state: SharedState,
) -> Result<Device, FactoryError> { ) -> Result<Device, FactoryError> {
let avtransport = Self::build_avtransport(pipeline.clone(), state.clone())?; 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 connectionmanager = Self::build_connectionmanager()?;
let short_name = extract_browser_name(browser_ua); let short_name = extract_browser_name(browser_ua);
@@ -147,10 +147,6 @@ impl WebRendererFactory {
state: SharedState, state: SharedState,
) -> Result<Service, FactoryError> { ) -> Result<Service, FactoryError> {
let mut svc = Service::new("AVTransport".to_string()); let mut svc = Service::new("AVTransport".to_string());
let add_var = |svc: &mut Service, var: &Arc<pmoupnp::state_variables::StateVariable>| {
svc.add_variable(Arc::clone(var))
.map_err(|e| FactoryError::VariableError(e.to_string()))
};
// Ajouter toutes les variables d'état // Ajouter toutes les variables d'état
add_var(&mut svc, &AVT_INSTANCE_ID)?; add_var(&mut svc, &AVT_INSTANCE_ID)?;
@@ -176,11 +172,6 @@ impl WebRendererFactory {
add_var(&mut svc, &TRANSPORTSTATE)?; add_var(&mut svc, &TRANSPORTSTATE)?;
add_var(&mut svc, &TRANSPORTSTATUS)?; add_var(&mut svc, &TRANSPORTSTATUS)?;
let add_action = |svc: &mut Service, action: Arc<Action>| {
svc.add_action(action)
.map_err(|e| FactoryError::ActionError(e.to_string()))
};
// Play // Play
let mut play = Action::new("Play".to_string()); let mut play = Action::new("Play".to_string());
add_arg_in(&mut play, "InstanceID", &AVT_INSTANCE_ID)?; add_arg_in(&mut play, "InstanceID", &AVT_INSTANCE_ID)?;
@@ -304,31 +295,21 @@ impl WebRendererFactory {
Ok(svc) Ok(svc)
} }
/// Construit le service RenderingControl avec les handlers pipeline /// Construit le service RenderingControl
fn build_renderingcontrol( fn build_renderingcontrol(state: SharedState) -> Result<Service, FactoryError> {
pipeline: PipelineHandle,
state: SharedState,
) -> Result<Service, FactoryError> {
let mut svc = Service::new("RenderingControl".to_string()); let mut svc = Service::new("RenderingControl".to_string());
svc.add_variable(Arc::clone(&RC_INSTANCE_ID)) add_var(&mut svc, &RC_INSTANCE_ID)?;
.map_err(|e| FactoryError::VariableError(e.to_string()))?; add_var(&mut svc, &A_ARG_TYPE_CHANNEL)?;
svc.add_variable(Arc::clone(&A_ARG_TYPE_CHANNEL)) add_var(&mut svc, &VOLUME)?;
.map_err(|e| FactoryError::VariableError(e.to_string()))?; add_var(&mut svc, &MUTE)?;
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()))?;
// SetVolume // SetVolume
let mut set_vol = Action::new("SetVolume".to_string()); 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, "InstanceID", &RC_INSTANCE_ID)?;
add_arg_in(&mut set_vol, "Channel", &A_ARG_TYPE_CHANNEL)?; add_arg_in(&mut set_vol, "Channel", &A_ARG_TYPE_CHANNEL)?;
add_arg_in(&mut set_vol, "DesiredVolume", &VOLUME)?; add_arg_in(&mut set_vol, "DesiredVolume", &VOLUME)?;
set_vol.set_handler(handlers::set_volume_handler( set_vol.set_handler(handlers::set_volume_handler(state.clone()));
pipeline.clone(),
state.clone(),
));
add_action(&mut svc, Arc::new(set_vol))?; add_action(&mut svc, Arc::new(set_vol))?;
// GetVolume // GetVolume
@@ -345,7 +326,7 @@ impl WebRendererFactory {
add_arg_in(&mut set_mute, "InstanceID", &RC_INSTANCE_ID)?; 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, "Channel", &A_ARG_TYPE_CHANNEL)?;
add_arg_in(&mut set_mute, "DesiredMute", &MUTE)?; 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))?; add_action(&mut svc, Arc::new(set_mute))?;
// GetMute // GetMute
@@ -355,8 +336,7 @@ impl WebRendererFactory {
add_arg_out(&mut get_mute, "CurrentMute", &MUTE)?; add_arg_out(&mut get_mute, "CurrentMute", &MUTE)?;
get_mute.set_stateful(false); get_mute.set_stateful(false);
get_mute.set_handler(handlers::get_mute_handler(state.clone())); get_mute.set_handler(handlers::get_mute_handler(state.clone()));
svc.add_action(Arc::new(get_mute)) add_action(&mut svc, Arc::new(get_mute))?;
.map_err(|e| FactoryError::ActionError(format!("{:?}", e)))?;
Ok(svc) Ok(svc)
} }