deps: update esbuild to 0.27.4, rollup to 4.60.0, vite to 7.3.1 - bump esbuild and all platform-specific binaries from 0.25.10 to 0.27.4 - bump rollup and all platform-specific binaries from 4.52.3 to 4.60.0 - bump vite from 7.1.7 to 7.3.1 (esbuild peer dep updated to ^0.27.0) - bump dompurify from 3.2.7 to 3.3.3 web: improve cover image retry logic - increase maxRetries default from 3 to 5 - reduce initial retryDelay from 1000ms to 500ms - implement exponential backoff: delay = retryDelay * 2^(retryCount - 1) - add detailed logging for retries and final failure rust: minor cleanup - remove unused imports (watch, Url, AudioSegment, SyncMarker) - add tracing debug logs for cache file requests - handle missing files gracefully with warning + client retry hint - add #[allow(async_fn_in_trait)] where needed
292 lines
10 KiB
Rust
292 lines
10 KiB
Rust
//! Action handlers SOAP → Pipeline pour le WebRenderer serveur
|
|
//!
|
|
//! 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 crate::messages::PlaybackState;
|
|
use crate::pipeline::{PipelineControl, PipelineHandle, upnp_time_to_seconds};
|
|
use crate::state::SharedState;
|
|
|
|
type ActionFuture =
|
|
std::pin::Pin<Box<dyn std::future::Future<Output = Result<ActionData, ActionError>> + Send>>;
|
|
|
|
// ─── AVTransport Handlers ───────────────────────────────────────────────────
|
|
|
|
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 {
|
|
// Ne pas écrire Playing ici : c'est stream_source qui le fera
|
|
// une fois que les premiers bytes FLAC ont été produits.
|
|
// Écrire Transitioning pour signaler que la lecture va démarrer.
|
|
state.write().playback_state = PlaybackState::Transitioning;
|
|
pipeline.send(PipelineControl::Play).await;
|
|
Ok(data)
|
|
})
|
|
})
|
|
}
|
|
|
|
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)
|
|
})
|
|
})
|
|
}
|
|
|
|
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)
|
|
})
|
|
})
|
|
}
|
|
|
|
pub fn next_handler(pipeline: PipelineHandle) -> ActionHandler {
|
|
Arc::new(move |data: ActionData| -> ActionFuture {
|
|
let pipeline = pipeline.clone();
|
|
Box::pin(async move {
|
|
pipeline.send(PipelineControl::Play).await;
|
|
Ok(data)
|
|
})
|
|
})
|
|
}
|
|
|
|
pub fn previous_handler(pipeline: PipelineHandle) -> ActionHandler {
|
|
Arc::new(move |data: ActionData| -> ActionFuture {
|
|
let pipeline = pipeline.clone();
|
|
Box::pin(async move {
|
|
pipeline.send(PipelineControl::Play).await;
|
|
Ok(data)
|
|
})
|
|
})
|
|
}
|
|
|
|
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)
|
|
})
|
|
})
|
|
}
|
|
|
|
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 {
|
|
let uri: String = get!(&data, "CurrentURI", String);
|
|
let metadata: String = get_value::<String>(&data, "CurrentURIMetaData")
|
|
.or_else(|_| {
|
|
get_value::<DIDLLite>(&data, "CurrentURIMetaData")
|
|
.map(|didl| didl.to_xml())
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
// Envoyer l'URI au pipeline serveur (remplace l'envoi WebSocket)
|
|
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)
|
|
})
|
|
})
|
|
}
|
|
|
|
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::<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;
|
|
|
|
{
|
|
let mut s = state.write();
|
|
s.next_uri = Some(uri);
|
|
s.next_metadata = Some(metadata);
|
|
}
|
|
Ok(data)
|
|
})
|
|
})
|
|
}
|
|
|
|
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)
|
|
})
|
|
})
|
|
}
|
|
|
|
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)
|
|
})
|
|
})
|
|
}
|
|
|
|
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)
|
|
})
|
|
})
|
|
}
|
|
|
|
// ─── RenderingControl Handlers ──────────────────────────────────────────────
|
|
|
|
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)
|
|
})
|
|
})
|
|
}
|
|
|
|
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)
|
|
})
|
|
})
|
|
}
|
|
|
|
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)
|
|
})
|
|
})
|
|
}
|
|
|
|
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 ─────────────────────────────────────────────
|
|
|
|
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());
|
|
// Serveur-side streaming : on produit du FLAC uniquement
|
|
set!(
|
|
&mut data,
|
|
"Sink",
|
|
"http-get:*:audio/flac:*,http-get:*:audio/x-flac:*".to_string()
|
|
);
|
|
Ok(data)
|
|
})
|
|
})
|
|
}
|