Files
pmomusic/pmoplaylist/src/sse.rs
2025-12-28 16:31:02 +01:00

92 lines
3.2 KiB
Rust

//! SSE pour suivre les évènements de playlists (updates + morceaux joués).
//!
//! Route type : `GET /api/playlists/events?playlist_id=foo`
use crate::{subscribe_events, PlaylistEventKind};
#[cfg(feature = "pmoserver")]
use async_stream::stream;
use axum::{
extract::Query,
response::sse::{Event, KeepAlive, Sse},
response::IntoResponse,
Router,
};
use serde::{Deserialize, Serialize};
#[cfg(feature = "pmoserver")]
#[derive(Debug, Default, Deserialize)]
#[cfg_attr(feature = "pmoserver", derive(utoipa::IntoParams, utoipa::ToSchema))]
pub struct EventsQuery {
/// Filtrer sur une playlist précise (optionnel).
#[serde(default)]
pub playlist_id: Option<String>,
}
#[derive(Debug, Serialize)]
#[cfg_attr(feature = "pmoserver", derive(utoipa::ToSchema))]
pub struct EventPayload {
pub playlist_id: String,
pub kind: String,
pub cache_pk: Option<String>,
pub qualifier: Option<String>,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub source_client: Option<String>,
}
/// Handler SSE : diffuse les évènements playlist enrichis.
#[utoipa::path(
get,
path = "/api/playlists/events",
tag = "playlists",
params(EventsQuery),
responses(
(status = 200, description = "Flux SSE des évènements playlists (updated, track_played)", content_type = "text/event-stream")
)
)]
pub async fn playlist_events_sse(Query(params): Query<EventsQuery>) -> impl IntoResponse {
let mut rx = subscribe_events();
let stream = stream! {
while let Ok(envelope) = rx.recv().await {
if let Some(filter) = &params.playlist_id {
if &envelope.event.playlist_id != filter {
continue;
}
}
let (kind, cache_pk, qualifier) = match &envelope.event.kind {
PlaylistEventKind::Updated => ("updated", None, None),
PlaylistEventKind::PkUpdated { old_pk: _, new_pk: _ } => {
// PK swap silencieux - envoyer quand même l'événement SSE pour debug/monitoring
("pk_updated", None, None)
}
PlaylistEventKind::TrackPlayed { cache_pk, qualifier } => {
("track_played", Some(cache_pk.as_str()), Some(qualifier.as_str()))
}
};
let ts = chrono::DateTime::<chrono::Utc>::from(envelope.timestamp);
let payload = EventPayload {
playlist_id: envelope.event.playlist_id.clone(),
kind: kind.to_string(),
cache_pk: cache_pk.map(|s| s.to_string()),
qualifier: qualifier.map(|s| s.to_string()),
timestamp: ts,
source_client: envelope.source_client.clone(),
};
if let Ok(json) = serde_json::to_string(&payload) {
yield Ok::<_, axum::Error>(Event::default().event("playlist").data(json));
}
}
};
Sse::new(stream).keep_alive(KeepAlive::default())
}
/// Router prêt à être monté (ex: `/api/playlists/events`).
pub fn playlist_events_router() -> Router {
use axum::routing::get;
Router::new().route("/events", get(playlist_events_sse))
}