debug lectueur générique

This commit is contained in:
2025-11-26 18:30:30 +01:00
parent d55c22a267
commit e21fa5948e
10 changed files with 583 additions and 92 deletions

View File

@@ -9,6 +9,6 @@ host:
logger:
buffer_capacity: 200
enable_console: true
min_level: INFO
min_level: trace
playlists:
directory: playlists

View File

@@ -62,10 +62,12 @@ async fn main() -> anyhow::Result<()> {
};
info!("Initializing Radio Paradise channels...");
let server_base_url = format!("http://localhost:{}", 8080);
let manager = Arc::new(
ParadiseChannelManager::with_defaults_with_cover_cache(
Some(cover_cache),
Some(history_builder),
Some(server_base_url),
)
.await?,
);

View File

@@ -5,7 +5,10 @@
use crate::channels::{ChannelDescriptor, ALL_CHANNELS};
use pmosource::pmodidl::{Container, Item, Resource};
use pmosource::{async_trait, BrowseResult, MusicSource, MusicSourceError, Result};
use pmosource::{
async_trait, AudioFormat, BrowseResult, MusicSource, MusicSourceError, Result,
SourceCapabilities,
};
use std::sync::Arc;
use std::time::SystemTime;
use tokio::sync::RwLock;
@@ -19,7 +22,7 @@ const DEFAULT_IMAGE: &[u8] = include_bytes!("../assets/default.webp");
/// RadioParadiseSource - UPnP ContentDirectory source for Radio Paradise
///
/// Provides access to:
/// - Live OGG streams for all 4 channels (Main, Mellow, Rock, Eclectic)
/// - Live FLAC streams for all 4 channels (Main, Mellow, Rock, Eclectic)
/// - Historical playlists (FIFO) for each channel
///
/// # Object ID Schema
@@ -60,9 +63,19 @@ impl RadioParadiseSource {
/// Build a live stream URL for a channel
fn build_live_url(&self, slug: &str) -> String {
format!("{}/radioparadise/stream/{}/flac", self.base_url, slug)
}
/// Build an OGG-FLAC live stream URL for clients that support it
fn build_live_ogg_url(&self, slug: &str) -> String {
format!("{}/radioparadise/stream/{}/ogg", self.base_url, slug)
}
/// URL de fallback pour l'image par défaut de la source
fn default_cover_url(&self) -> String {
format!("{}/api/sources/{}/image", self.base_url, self.id())
}
/// Fetch current metadata from the live stream
async fn fetch_live_metadata(&self, slug: &str) -> Result<Option<Item>> {
let metadata_url = format!("{}/radioparadise/metadata/{}", self.base_url, slug);
@@ -77,7 +90,13 @@ impl RadioParadiseSource {
let artist = json["artist"].as_str().map(|s| s.to_string());
let album = json["album"].as_str().map(|s| s.to_string());
let year = json["year"].as_u64().map(|y| y as u32);
let cover_url = json["cover_url"].as_str().map(|s| s.to_string());
// Préférer l'URL de cache si cover_pk est fourni par le pipeline
let cover_pk = json["cover_pk"].as_str().map(|s| s.to_string());
let cover_url = cover_pk
.as_ref()
.map(|pk| format!("{}/covers/image/{}", self.base_url, pk))
.or_else(|| json["cover_url"].as_str().map(|s| s.to_string()))
.or_else(|| Some(self.default_cover_url()));
// Parse duration from JSON (in seconds as a float)
let duration = json["duration"]
@@ -105,11 +124,11 @@ impl RadioParadiseSource {
album,
genre: Some("Radio".to_string()),
album_art: cover_url,
album_art_pk: None,
album_art_pk: cover_pk,
date: year.map(|y| y.to_string()),
original_track_number: None,
resources: vec![Resource {
protocol_info: "http-get:*:audio/ogg:*".to_string(),
protocol_info: "http-get:*:audio/flac:*".to_string(),
bits_per_sample: None,
sample_frequency: None,
nr_audio_channels: Some("2".to_string()),
@@ -192,18 +211,28 @@ impl RadioParadiseSource {
artist: Some("Radio Paradise".to_string()),
album: Some(descriptor.display_name.to_string()),
genre: Some("Radio".to_string()),
album_art: None,
album_art: Some(self.default_cover_url()),
album_art_pk: None,
date: None,
original_track_number: None,
resources: vec![Resource {
protocol_info: "http-get:*:audio/ogg:*".to_string(),
bits_per_sample: None,
sample_frequency: None,
nr_audio_channels: Some("2".to_string()),
duration: None,
url: stream_url,
}],
resources: vec![
Resource {
protocol_info: "http-get:*:audio/flac:*".to_string(),
bits_per_sample: Some("16".to_string()),
sample_frequency: Some("44100".to_string()),
nr_audio_channels: Some("2".to_string()),
duration: None,
url: stream_url.clone(),
},
Resource {
protocol_info: "http-get:*:audio/ogg:*".to_string(),
bits_per_sample: Some("16".to_string()),
sample_frequency: Some("44100".to_string()),
nr_audio_channels: Some("2".to_string()),
duration: None,
url: self.build_live_ogg_url(descriptor.slug),
},
],
descriptions: vec![],
}
}
@@ -415,6 +444,56 @@ impl MusicSource for RadioParadiseSource {
}
}
fn capabilities(&self) -> SourceCapabilities {
SourceCapabilities {
supports_fifo: self.supports_fifo(),
supports_search: false,
supports_favorites: false,
supports_playlists: false,
supports_user_content: false,
supports_high_res_audio: true,
max_sample_rate: Some(44100),
supports_multiple_formats: true,
supports_advanced_search: false,
supports_pagination: false,
}
}
async fn get_available_formats(&self, object_id: &str) -> Result<Vec<AudioFormat>> {
match Self::parse_object_id(object_id) {
ObjectIdType::LiveStream { .. } => Ok(vec![
AudioFormat {
format_id: "flac".to_string(),
mime_type: "audio/flac".to_string(),
sample_rate: Some(44100),
bit_depth: Some(16),
bitrate: None,
channels: Some(2),
},
AudioFormat {
format_id: "ogg-flac".to_string(),
mime_type: "audio/ogg".to_string(),
sample_rate: Some(44100),
bit_depth: Some(16),
bitrate: None,
channels: Some(2),
},
]),
ObjectIdType::HistoryTrack { .. } => Ok(vec![AudioFormat {
format_id: "flac".to_string(),
mime_type: "audio/flac".to_string(),
sample_rate: Some(44100),
bit_depth: Some(16),
bitrate: None,
channels: Some(2),
}]),
_ => Err(MusicSourceError::ObjectNotFound(format!(
"Cannot list formats for object: {}",
object_id
))),
}
}
async fn get_item(&self, object_id: &str) -> Result<Item> {
match Self::parse_object_id(object_id) {
ObjectIdType::LiveStream { slug } => {

View File

@@ -12,7 +12,7 @@ use std::{
Arc,
},
task::{Context, Poll},
time::{Duration, SystemTime, UNIX_EPOCH},
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
use crate::{
@@ -48,14 +48,17 @@ pub struct ParadiseStreamChannelConfig {
pub flac_options: StreamingSinkOptions,
/// Options pour le flux OGG-FLAC.
pub ogg_options: StreamingSinkOptions,
/// URL de base du serveur (pour les métadonnées, covers...)
pub server_base_url: Option<String>,
}
impl Default for ParadiseStreamChannelConfig {
fn default() -> Self {
Self {
max_lead_seconds: 1.0,
max_lead_seconds: 3.0, // Compromis live/fluidité : assez pour absorber les transitions
flac_options: StreamingSinkOptions::flac_defaults(),
ogg_options: StreamingSinkOptions::ogg_defaults(),
server_base_url: None,
}
}
}
@@ -91,7 +94,7 @@ impl ParadiseHistoryBuilder {
playlist_title_prefix: Some("Radio Paradise History".into()),
max_history_tracks: Some(500),
collection_prefix: Some("radio-paradise".into()),
replay_max_lead_seconds: 1.0,
replay_max_lead_seconds: 3.0, // Aligné avec le live
}
}
@@ -145,6 +148,7 @@ impl ParadiseStreamChannelConfig {
max_lead_seconds: v.max(0.1),
flac_options: StreamingSinkOptions::flac_defaults(),
ogg_options: StreamingSinkOptions::ogg_defaults(),
server_base_url: None,
}
} else {
let default = Self::default();
@@ -159,6 +163,7 @@ impl ParadiseStreamChannelConfig {
max_lead_seconds: v.max(0.1),
flac_options: StreamingSinkOptions::flac_defaults(),
ogg_options: StreamingSinkOptions::ogg_defaults(),
server_base_url: None,
}
} else {
let default = Self::default();
@@ -195,6 +200,13 @@ impl ParadiseStreamChannel {
cover_cache: Option<Arc<CoverCache>>,
history: Option<ParadiseHistoryOptions>,
) -> Result<Self> {
// Propager server_base_url dans les options pour que les encoders injectent les covers du cache
let mut config = config;
if let Some(ref base) = config.server_base_url {
config.flac_options =
config.flac_options.clone().with_server_base_url(Some(base.clone()));
config.ogg_options = config.ogg_options.clone().with_server_base_url(Some(base.clone()));
}
let cover_cache = cover_cache
.or_else(|| history.as_ref().map(|opts| opts.cover_cache.clone()))
.or_else(|| get_cover_cache());
@@ -813,10 +825,31 @@ impl ParadiseChannelManager {
pub async fn with_defaults_with_cover_cache(
cover_cache: Option<Arc<CoverCache>>,
history_builder: Option<ParadiseHistoryBuilder>,
server_base_url: Option<String>,
) -> Result<Self> {
tracing::warn!(
"➡️ Entering with_defaults_with_cover_cache ({} channels, base_url={:?})",
ALL_CHANNELS.len(),
server_base_url
);
let mut map = HashMap::new();
for descriptor in ALL_CHANNELS.iter().copied() {
let mut config = ParadiseStreamChannelConfig::default();
config.server_base_url = server_base_url.clone();
let start = Instant::now();
tracing::warn!(
"⏳ Initializing Radio Paradise channel {} ({})...",
descriptor.display_name,
descriptor.slug
);
let history_opts = if let Some(builder) = &history_builder {
tracing::warn!(
" ⏳ Building history options for channel {} ({})",
descriptor.display_name,
descriptor.slug
);
Some(
builder
.build_for_channel(&descriptor)
@@ -826,20 +859,56 @@ impl ParadiseChannelManager {
} else {
None
};
let channel = ParadiseStreamChannel::new(
descriptor,
ParadiseStreamChannelConfig::default(),
cover_cache.clone(),
history_opts,
tracing::warn!(
" ⏩ History options ready for channel {} ({})",
descriptor.display_name,
descriptor.slug
);
let channel = match tokio::time::timeout(
Duration::from_secs(20),
ParadiseStreamChannel::new(
descriptor,
config,
cover_cache.clone(),
history_opts,
),
)
.await?;
.await
{
Ok(Ok(ch)) => {
tracing::warn!(
"✅ Channel {} ({}) initialized in {:?}",
descriptor.display_name,
descriptor.slug,
start.elapsed()
);
ch
}
Ok(Err(e)) => {
tracing::error!(
"⚠️ Failed to initialize channel {} ({}): {}",
descriptor.display_name,
descriptor.slug,
e
);
continue;
}
Err(_) => {
tracing::error!(
"⚠️ Timeout initializing channel {} ({}) after 20s, skipping",
descriptor.display_name,
descriptor.slug
);
continue;
}
};
map.insert(descriptor.id, Arc::new(channel));
}
Ok(Self { channels: map })
}
pub async fn with_defaults() -> Result<Self> {
Self::with_defaults_with_cover_cache(None, None).await
Self::with_defaults_with_cover_cache(None, None, None).await
}
pub fn get(&self, id: u8) -> Option<Arc<ParadiseStreamChannel>> {

View File

@@ -39,6 +39,7 @@ pub struct ParadiseStreamChannelConfig {
pub max_lead_seconds: f64,
pub flac_options: StreamingSinkOptions,
pub ogg_options: StreamingSinkOptions,
pub server_base_url: Option<String>,
}
impl Default for ParadiseStreamChannelConfig {
@@ -47,6 +48,7 @@ impl Default for ParadiseStreamChannelConfig {
max_lead_seconds: 1.0,
flac_options: StreamingSinkOptions::flac_defaults(),
ogg_options: StreamingSinkOptions::ogg_defaults(),
server_base_url: None,
}
}
}
@@ -143,6 +145,9 @@ impl ParadiseStreamChannelConfig {
if let Some(v) = num.as_f64() {
Self {
max_lead_seconds: v.max(0.1),
flac_options: StreamingSinkOptions::flac_defaults(),
ogg_options: StreamingSinkOptions::ogg_defaults(),
server_base_url: None,
}
} else {
let default = Self::default();
@@ -155,6 +160,9 @@ impl ParadiseStreamChannelConfig {
if let Ok(v) = s.parse::<f64>() {
Self {
max_lead_seconds: v.max(0.1),
flac_options: StreamingSinkOptions::flac_defaults(),
ogg_options: StreamingSinkOptions::ogg_defaults(),
server_base_url: None,
}
} else {
let default = Self::default();
@@ -666,9 +674,13 @@ impl ParadiseChannelManager {
pub async fn with_defaults_with_cover_cache(
cover_cache: Option<Arc<CoverCache>>,
history_builder: Option<ParadiseHistoryBuilder>,
server_base_url: Option<String>,
) -> Result<Self> {
let mut map = HashMap::new();
for descriptor in ALL_CHANNELS.iter().copied() {
let mut config = ParadiseStreamChannelConfig::default();
config.server_base_url = server_base_url.clone();
let history_opts = if let Some(builder) = &history_builder {
Some(
builder
@@ -681,7 +693,7 @@ impl ParadiseChannelManager {
};
let channel = ParadiseStreamChannel::new(
descriptor,
ParadiseStreamChannelConfig::default(),
config,
cover_cache.clone(),
history_opts,
)
@@ -692,7 +704,7 @@ impl ParadiseChannelManager {
}
pub async fn with_defaults() -> Result<Self> {
Self::with_defaults_with_cover_cache(None, None).await
Self::with_defaults_with_cover_cache(None, None, None).await
}
pub fn get(&self, id: u8) -> Option<Arc<ParadiseStreamChannel>> {