Bump version to 0.3.12 and enhance Radio France source integration
Bump version from 0.3.11 to 0.3.12 - Update Cargo.toml and version.txt to 0.3.12 - Refactor Radio France source registration to share Arc with API - Enhance API routes to start/stop metadata refresh on stream access - Add cleanup logic for stream disconnection - Improve logging for metadata refresh operations - Update Radio France source methods to be public for external access
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -4,7 +4,7 @@ version = 4
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "PMOMusic"
|
name = "PMOMusic"
|
||||||
version = "0.3.11"
|
version = "0.3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum 0.8.7",
|
"axum 0.8.7",
|
||||||
"console-subscriber",
|
"console-subscriber",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "PMOMusic"
|
name = "PMOMusic"
|
||||||
version = "0.3.11"
|
version = "0.3.12"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -270,11 +270,14 @@ impl SourcesExt for Server {
|
|||||||
});
|
});
|
||||||
let source = source.with_container_notifier(notifier);
|
let source = source.with_container_notifier(notifier);
|
||||||
|
|
||||||
// Enregistrer la source
|
// Enregistrer la source (Arc pour partage avec l'API)
|
||||||
self.register_music_source(Arc::new(source)).await;
|
let source_arc = Arc::new(source);
|
||||||
|
self.register_music_source(source_arc.clone()).await;
|
||||||
|
|
||||||
// Initialiser les routes API Radio France
|
// Initialiser les routes API Radio France avec la source
|
||||||
self.init_radiofrance().await.map_err(|e| {
|
self.init_radiofrance_with_source(source_arc)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
SourceInitError::RadioFranceError(format!("Failed to init API routes: {}", e))
|
SourceInitError::RadioFranceError(format!("Failed to init API routes: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
// ============ Gestion des erreurs ============
|
// ============ Gestion des erreurs ============
|
||||||
|
|
||||||
@@ -95,6 +96,25 @@ async fn proxy_stream(
|
|||||||
State(state): State<RadioFranceState>,
|
State(state): State<RadioFranceState>,
|
||||||
Path(slug): Path<String>,
|
Path(slug): Path<String>,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
|
// Start metadata refresh when stream is accessed
|
||||||
|
#[cfg(feature = "logging")]
|
||||||
|
tracing::info!("Stream proxy accessed for station: {}", slug);
|
||||||
|
|
||||||
|
if let Some(ref source) = state.source {
|
||||||
|
// Spawn refresh task (non-blocking)
|
||||||
|
let source_clone = Arc::clone(source);
|
||||||
|
let slug_clone = slug.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = source_clone.start_metadata_refresh(&slug_clone).await {
|
||||||
|
#[cfg(feature = "logging")]
|
||||||
|
tracing::error!("Failed to start metadata refresh for {}: {}", slug_clone, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
#[cfg(feature = "logging")]
|
||||||
|
tracing::warn!("No source available to start metadata refresh");
|
||||||
|
}
|
||||||
|
|
||||||
// Get the stream URL
|
// Get the stream URL
|
||||||
let stream_url = state
|
let stream_url = state
|
||||||
.client
|
.client
|
||||||
@@ -116,12 +136,48 @@ async fn proxy_stream(
|
|||||||
headers.insert("content-type", "audio/aac".parse().unwrap());
|
headers.insert("content-type", "audio/aac".parse().unwrap());
|
||||||
headers.insert("cache-control", "no-cache".parse().unwrap());
|
headers.insert("cache-control", "no-cache".parse().unwrap());
|
||||||
|
|
||||||
// Create streaming body
|
// Create streaming body with cleanup on disconnect
|
||||||
let stream = response
|
let stream = response
|
||||||
.bytes_stream()
|
.bytes_stream()
|
||||||
.map(|chunk| chunk.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)));
|
.map(|chunk| chunk.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)));
|
||||||
|
|
||||||
let body = Body::from_stream(stream);
|
// Wrap the stream to detect when client disconnects
|
||||||
|
let source_for_cleanup = state.source.clone();
|
||||||
|
let slug_for_cleanup = slug.clone();
|
||||||
|
let monitored_stream =
|
||||||
|
futures::stream::unfold((stream, false), move |(mut stream, mut done)| {
|
||||||
|
let source = source_for_cleanup.clone();
|
||||||
|
let slug = slug_for_cleanup.clone();
|
||||||
|
async move {
|
||||||
|
if done {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
match stream.next().await {
|
||||||
|
Some(Ok(chunk)) => Some((Ok(chunk), (stream, false))),
|
||||||
|
Some(Err(e)) => {
|
||||||
|
// Error occurred, stop refresh
|
||||||
|
#[cfg(feature = "logging")]
|
||||||
|
tracing::info!("Stream error for {}, stopping refresh", slug);
|
||||||
|
if let Some(src) = source {
|
||||||
|
src.stop_metadata_refresh(&slug).await;
|
||||||
|
}
|
||||||
|
Some((Err(e), (stream, true)))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Stream ended normally, stop refresh
|
||||||
|
#[cfg(feature = "logging")]
|
||||||
|
tracing::info!("Stream ended for {}, stopping refresh", slug);
|
||||||
|
if let Some(src) = source {
|
||||||
|
src.stop_metadata_refresh(&slug).await;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let body = Body::from_stream(monitored_stream);
|
||||||
|
|
||||||
Ok((headers, body).into_response())
|
Ok((headers, body).into_response())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,14 +12,21 @@ use crate::stateful_client::RadioFranceStatefulClient;
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RadioFranceState {
|
pub struct RadioFranceState {
|
||||||
pub client: Arc<RadioFranceStatefulClient>,
|
pub client: Arc<RadioFranceStatefulClient>,
|
||||||
|
pub source: Option<Arc<crate::source::RadioFranceSource>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RadioFranceState {
|
impl RadioFranceState {
|
||||||
pub fn new(client: RadioFranceStatefulClient) -> Self {
|
pub fn new(client: RadioFranceStatefulClient) -> Self {
|
||||||
Self {
|
Self {
|
||||||
client: Arc::new(client),
|
client: Arc::new(client),
|
||||||
|
source: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_source(mut self, source: Arc<crate::source::RadioFranceSource>) -> Self {
|
||||||
|
self.source = Some(source);
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait pour étendre pmoserver avec les fonctionnalités Radio France
|
/// Trait pour étendre pmoserver avec les fonctionnalités Radio France
|
||||||
@@ -80,6 +87,16 @@ pub trait RadioFranceExt {
|
|||||||
/// server.init_radiofrance().await?;
|
/// server.init_radiofrance().await?;
|
||||||
/// ```
|
/// ```
|
||||||
async fn init_radiofrance(&mut self) -> Result<Arc<RadioFranceState>>;
|
async fn init_radiofrance(&mut self) -> Result<Arc<RadioFranceState>>;
|
||||||
|
|
||||||
|
/// Initialise l'extension Radio France avec une source existante
|
||||||
|
///
|
||||||
|
/// Cette méthode est similaire à `init_radiofrance()` mais utilise une source
|
||||||
|
/// déjà créée et enregistrée, permettant de partager la même instance entre
|
||||||
|
/// le MediaServer UPnP et les routes API REST.
|
||||||
|
async fn init_radiofrance_with_source(
|
||||||
|
&mut self,
|
||||||
|
source: Arc<crate::source::RadioFranceSource>,
|
||||||
|
) -> Result<Arc<RadioFranceState>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// L'implémentation du trait sera dans un module séparé (pmoserver_impl.rs)
|
// L'implémentation du trait sera dans un module séparé (pmoserver_impl.rs)
|
||||||
|
|||||||
@@ -56,4 +56,29 @@ impl RadioFranceExt for Server {
|
|||||||
|
|
||||||
Ok(Arc::new(state))
|
Ok(Arc::new(state))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn init_radiofrance_with_source(
|
||||||
|
&mut self,
|
||||||
|
source: Arc<crate::source::RadioFranceSource>,
|
||||||
|
) -> Result<Arc<RadioFranceState>> {
|
||||||
|
info!("Initializing Radio France API with existing source...");
|
||||||
|
|
||||||
|
// Créer le client stateful
|
||||||
|
let config = pmoconfig::get_config();
|
||||||
|
let client = RadioFranceStatefulClient::new(config)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to create Radio France client: {}", e))?;
|
||||||
|
|
||||||
|
// Créer l'état partagé avec la source
|
||||||
|
let state = RadioFranceState::new(client).with_source(source);
|
||||||
|
|
||||||
|
// Créer et enregistrer le router
|
||||||
|
let router = create_router(state.clone());
|
||||||
|
self.add_router("/api/radiofrance", router).await;
|
||||||
|
|
||||||
|
info!("Radio France API initialized with source");
|
||||||
|
info!("API endpoints available at /api/radiofrance/*");
|
||||||
|
|
||||||
|
Ok(Arc::new(state))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ impl RadioFranceSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Start metadata refresh task for a station
|
/// Start metadata refresh task for a station
|
||||||
async fn start_metadata_refresh(&self, station_slug: &str) -> Result<()> {
|
pub async fn start_metadata_refresh(&self, station_slug: &str) -> Result<()> {
|
||||||
let mut handles = self.refresh_handles.write().await;
|
let mut handles = self.refresh_handles.write().await;
|
||||||
|
|
||||||
// If already running, do nothing
|
// If already running, do nothing
|
||||||
@@ -177,11 +177,51 @@ impl RadioFranceSource {
|
|||||||
Ok(metadata) => {
|
Ok(metadata) => {
|
||||||
let delay = std::time::Duration::from_millis(metadata.delay_to_refresh);
|
let delay = std::time::Duration::from_millis(metadata.delay_to_refresh);
|
||||||
|
|
||||||
|
#[cfg(feature = "logging")]
|
||||||
|
{
|
||||||
|
let artist = metadata
|
||||||
|
.now
|
||||||
|
.song
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|s| {
|
||||||
|
if s.interpreters.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(s.artists_display())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "".to_string());
|
||||||
|
tracing::debug!(
|
||||||
|
"Refreshed metadata for {}: title='{}' artist='{}' delay={}ms",
|
||||||
|
slug,
|
||||||
|
metadata.now.first_line.title.as_deref().unwrap_or(""),
|
||||||
|
artist,
|
||||||
|
metadata.delay_to_refresh
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Update the playlist metadata
|
// Update the playlist metadata
|
||||||
#[cfg(feature = "cache")]
|
#[cfg(feature = "cache")]
|
||||||
{
|
{
|
||||||
let mut pls = playlists.write().await;
|
let mut pls = playlists.write().await;
|
||||||
|
|
||||||
|
#[cfg(feature = "logging")]
|
||||||
|
tracing::debug!(
|
||||||
|
"Looking for playlist '{}' in cache, found: {}",
|
||||||
|
slug,
|
||||||
|
pls.contains_key(&slug)
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(playlist) = pls.get_mut(&slug) {
|
if let Some(playlist) = pls.get_mut(&slug) {
|
||||||
|
let old_title = playlist.stream_item.title.clone();
|
||||||
|
|
||||||
|
#[cfg(feature = "logging")]
|
||||||
|
tracing::debug!(
|
||||||
|
"Updating playlist for {}: current title = '{}'",
|
||||||
|
slug,
|
||||||
|
old_title
|
||||||
|
);
|
||||||
|
|
||||||
let _: Result<()> = playlist
|
let _: Result<()> = playlist
|
||||||
.update_metadata(
|
.update_metadata(
|
||||||
&metadata,
|
&metadata,
|
||||||
@@ -190,6 +230,18 @@ impl RadioFranceSource {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
let new_title = playlist.stream_item.title.clone();
|
||||||
|
|
||||||
|
#[cfg(feature = "logging")]
|
||||||
|
if old_title != new_title {
|
||||||
|
tracing::info!(
|
||||||
|
"Metadata updated for {}: {} -> {}",
|
||||||
|
slug,
|
||||||
|
old_title,
|
||||||
|
new_title
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Update change tracking
|
// Update change tracking
|
||||||
*update_id.write().await = update_id.read().await.wrapping_add(1);
|
*update_id.write().await = update_id.read().await.wrapping_add(1);
|
||||||
*last_change.write().await = Some(SystemTime::now());
|
*last_change.write().await = Some(SystemTime::now());
|
||||||
@@ -198,6 +250,13 @@ impl RadioFranceSource {
|
|||||||
if let Some(ref notifier) = container_notifier {
|
if let Some(ref notifier) = container_notifier {
|
||||||
// Notify the station's stream item container
|
// Notify the station's stream item container
|
||||||
let container_id = format!("radiofrance:{}", slug);
|
let container_id = format!("radiofrance:{}", slug);
|
||||||
|
|
||||||
|
#[cfg(feature = "logging")]
|
||||||
|
tracing::info!(
|
||||||
|
"Notifying UPnP container update: {}",
|
||||||
|
container_id
|
||||||
|
);
|
||||||
|
|
||||||
notifier(&[container_id]);
|
notifier(&[container_id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,12 +265,41 @@ impl RadioFranceSource {
|
|||||||
#[cfg(not(feature = "cache"))]
|
#[cfg(not(feature = "cache"))]
|
||||||
{
|
{
|
||||||
let mut pls = playlists.write().await;
|
let mut pls = playlists.write().await;
|
||||||
|
|
||||||
|
#[cfg(feature = "logging")]
|
||||||
|
tracing::debug!(
|
||||||
|
"Looking for playlist '{}' in cache, found: {}",
|
||||||
|
slug,
|
||||||
|
pls.contains_key(&slug)
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(playlist) = pls.get_mut(&slug) {
|
if let Some(playlist) = pls.get_mut(&slug) {
|
||||||
|
let old_title = playlist.stream_item.title.clone();
|
||||||
|
|
||||||
|
#[cfg(feature = "logging")]
|
||||||
|
tracing::debug!(
|
||||||
|
"Updating playlist for {}: current title = '{}'",
|
||||||
|
slug,
|
||||||
|
old_title
|
||||||
|
);
|
||||||
|
|
||||||
let _: Result<()> = playlist.update_metadata_no_cache(
|
let _: Result<()> = playlist.update_metadata_no_cache(
|
||||||
&metadata,
|
&metadata,
|
||||||
server_base_url.as_deref(),
|
server_base_url.as_deref(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let new_title = playlist.stream_item.title.clone();
|
||||||
|
|
||||||
|
#[cfg(feature = "logging")]
|
||||||
|
if old_title != new_title {
|
||||||
|
tracing::info!(
|
||||||
|
"Metadata updated for {}: {} -> {}",
|
||||||
|
slug,
|
||||||
|
old_title,
|
||||||
|
new_title
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Update change tracking
|
// Update change tracking
|
||||||
*update_id.write().await = update_id.read().await.wrapping_add(1);
|
*update_id.write().await = update_id.read().await.wrapping_add(1);
|
||||||
*last_change.write().await = Some(SystemTime::now());
|
*last_change.write().await = Some(SystemTime::now());
|
||||||
@@ -220,6 +308,13 @@ impl RadioFranceSource {
|
|||||||
if let Some(ref notifier) = container_notifier {
|
if let Some(ref notifier) = container_notifier {
|
||||||
// Notify the station's stream item container
|
// Notify the station's stream item container
|
||||||
let container_id = format!("radiofrance:{}", slug);
|
let container_id = format!("radiofrance:{}", slug);
|
||||||
|
|
||||||
|
#[cfg(feature = "logging")]
|
||||||
|
tracing::info!(
|
||||||
|
"Notifying UPnP container update: {}",
|
||||||
|
container_id
|
||||||
|
);
|
||||||
|
|
||||||
notifier(&[container_id]);
|
notifier(&[container_id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,7 +341,7 @@ impl RadioFranceSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Stop metadata refresh task for a station
|
/// Stop metadata refresh task for a station
|
||||||
async fn stop_metadata_refresh(&self, station_slug: &str) {
|
pub async fn stop_metadata_refresh(&self, station_slug: &str) {
|
||||||
let mut handles = self.refresh_handles.write().await;
|
let mut handles = self.refresh_handles.write().await;
|
||||||
if let Some(handle) = handles.remove(station_slug) {
|
if let Some(handle) = handles.remove(station_slug) {
|
||||||
handle.abort();
|
handle.abort();
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
0.3.11
|
0.3.12
|
||||||
|
|||||||
Reference in New Issue
Block a user