debug lectueur générique
This commit is contained in:
@@ -9,6 +9,6 @@ host:
|
||||
logger:
|
||||
buffer_capacity: 200
|
||||
enable_console: true
|
||||
min_level: INFO
|
||||
min_level: trace
|
||||
playlists:
|
||||
directory: playlists
|
||||
|
||||
@@ -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?,
|
||||
);
|
||||
|
||||
@@ -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 } => {
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
Reference in New Issue
Block a user