lastest correction on webapp

This commit is contained in:
2025-10-19 23:48:56 +02:00
parent 2632233dbd
commit b6e439dcf6
7 changed files with 680 additions and 74 deletions

View File

@@ -28,7 +28,9 @@
//!
//! if let Some(song) = &now_playing.current_song {
//! println!("Now Playing: {} - {}", song.artist, song.title);
//! println!("Album: {}", song.album);
//! if let Some(album) = &song.album {
//! println!("Album: {}", album);
//! }
//! }
//!
//! // Get all songs in the current block
@@ -189,33 +191,24 @@
//! # #[cfg(feature = "cache")]
//! # {
//! use pmoparadise::{RadioParadiseClient, RadioParadiseSource};
//! use pmocovers::Cache as CoverCache;
//! use pmoaudiocache::AudioCache;
//! use std::sync::Arc;
//!
//! #[tokio::main]
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! // Create caches
//! let cover_cache = Arc::new(CoverCache::new("./cache/covers", 500)?);
//! let audio_cache = Arc::new(AudioCache::new("./cache/audio", 100)?);
//! let cover_cache = Arc::new(pmocovers::cache::new_cache("./cache/covers", 500)?);
//! let audio_cache = Arc::new(pmoaudiocache::cache::new_cache("./cache/audio", 100)?);
//!
//! // Create client and source with caching
//! let client = RadioParadiseClient::new().await?;
//! let source = RadioParadiseSource::new_with_cache(
//! client.clone(),
//! "http://localhost:8080",
//! let source = RadioParadiseSource::new(
//! client,
//! 50,
//! Some(cover_cache),
//! Some(audio_cache),
//! cover_cache,
//! audio_cache,
//! );
//!
//! // Add songs - they will be automatically cached
//! let now_playing = client.now_playing().await?;
//! if let Some(song) = &now_playing.current_song {
//! let block = Arc::new(now_playing.block.clone());
//! source.add_song(block, song, 0).await?;
//! // Cover and audio are now cached!
//! }
//! println!("Source ready: {}", source.name());
//!
//! Ok(())
//! }

View File

@@ -1,6 +1,7 @@
//! Data models for Radio Paradise API responses
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Number;
use std::collections::HashMap;
/// Deserialize a string or number into a u64
@@ -34,17 +35,31 @@ where
#[serde(untagged)]
enum StringOrNumber {
String(String),
Float(f64),
Int(u64),
Number(Number),
}
fn to_milliseconds(value: f64) -> u64 {
if value >= 100_000.0 {
value.round() as u64
} else {
(value * 1000.0).round() as u64
}
}
match StringOrNumber::deserialize(deserializer)? {
StringOrNumber::String(s) => {
let seconds = s.parse::<f64>().map_err(D::Error::custom)?;
Ok((seconds * 1000.0) as u64)
let value = s.parse::<f64>().map_err(D::Error::custom)?;
Ok(to_milliseconds(value))
}
StringOrNumber::Number(n) => {
if let Some(int_value) = n.as_u64() {
Ok(to_milliseconds(int_value as f64))
} else if let Some(float_value) = n.as_f64() {
Ok(to_milliseconds(float_value))
} else {
Err(D::Error::custom("Invalid number for block length"))
}
}
StringOrNumber::Float(f) => Ok((f * 1000.0) as u64),
StringOrNumber::Int(i) => Ok(i),
}
}
@@ -435,4 +450,60 @@ mod tests {
assert_eq!(idx, 1);
assert_eq!(song.title, "Giant Steps");
}
#[test]
fn test_block_length_from_seconds_string() {
let json = serde_json::json!({
"event": 1,
"end_event": 2,
"length": "1715.54",
"url": "https://example.com/block.flac",
"song": {}
});
let block: Block = serde_json::from_value(json).unwrap();
assert_eq!(block.length, 1_715_540);
}
#[test]
fn test_block_length_from_seconds_integer() {
let json = serde_json::json!({
"event": 1,
"end_event": 2,
"length": 1800,
"url": "https://example.com/block.flac",
"song": {}
});
let block: Block = serde_json::from_value(json).unwrap();
assert_eq!(block.length, 1_800_000);
}
#[test]
fn test_block_length_from_milliseconds_integer() {
let json = serde_json::json!({
"event": 1,
"end_event": 2,
"length": 900_000,
"url": "https://example.com/block.flac",
"song": {}
});
let block: Block = serde_json::from_value(json).unwrap();
assert_eq!(block.length, 900_000);
}
#[test]
fn test_block_length_from_milliseconds_float() {
let json = serde_json::json!({
"event": 1,
"end_event": 2,
"length": 900_000.0,
"url": "https://example.com/block.flac",
"song": {}
});
let block: Block = serde_json::from_value(json).unwrap();
assert_eq!(block.length, 900_000);
}
}

View File

@@ -217,6 +217,10 @@ impl From<NowPlaying> for NowPlayingResponse {
#[utoipa::path(
get,
path = "/now-playing",
params(
("channel" = Option<u8>, Query, description = "Channel ID (0-3)"),
("bitrate" = Option<u8>, Query, description = "Bitrate ID (0-4)")
),
responses(
(status = 200, description = "Morceau en cours", body = NowPlayingResponse),
(status = 500, description = "Erreur serveur")
@@ -240,6 +244,10 @@ async fn get_now_playing(
#[utoipa::path(
get,
path = "/block/current",
params(
("channel" = Option<u8>, Query, description = "Channel ID (0-3)"),
("bitrate" = Option<u8>, Query, description = "Bitrate ID (0-4)")
),
responses(
(status = 200, description = "Block actuel", body = BlockResponse),
(status = 500, description = "Erreur serveur")
@@ -264,7 +272,9 @@ async fn get_current_block(
get,
path = "/block/{event_id}",
params(
("event_id" = u64, Path, description = "Event ID du block")
("event_id" = u64, Path, description = "Event ID du block"),
("channel" = Option<u8>, Query, description = "Channel ID (0-3)"),
("bitrate" = Option<u8>, Query, description = "Bitrate ID (0-4)")
),
responses(
(status = 200, description = "Block demandé", body = BlockResponse),

View File

@@ -114,13 +114,26 @@ fn parse_track_identifier(track_id: &str) -> Option<(u8, u64, usize)> {
/// # Examples
///
/// ```no_run
/// use pmoparadise::{RadioParadiseSource, RadioParadiseClient};
/// use pmoparadise::{RadioParadiseClient, RadioParadiseSource};
/// use pmosource::MusicSource;
/// use std::sync::Arc;
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let client = RadioParadiseClient::new().await?;
/// let source = RadioParadiseSource::new(client, "http://localhost:8080", 50);
///
/// let base_dir = std::env::temp_dir().join("pmoparadise_doc_source");
/// let cover_dir = base_dir.join("covers");
/// let audio_dir = base_dir.join("audio");
/// std::fs::create_dir_all(&cover_dir)?;
/// std::fs::create_dir_all(&audio_dir)?;
///
/// let cover_dir_str = cover_dir.to_string_lossy().into_owned();
/// let audio_dir_str = audio_dir.to_string_lossy().into_owned();
/// let cover_cache = Arc::new(pmocovers::cache::new_cache(&cover_dir_str, 32)?);
/// let audio_cache = Arc::new(pmoaudiocache::cache::new_cache(&audio_dir_str, 32)?);
///
/// let source = RadioParadiseSource::new(client, 50, cover_cache, audio_cache);
///
/// println!("Source: {}", source.name());
/// println!("Supports FIFO: {}", source.supports_fifo());
@@ -431,14 +444,13 @@ impl RadioParadiseSource {
.map_err(|e| MusicSourceError::CacheError(e.to_string()))?;
let audio_source_uri = format!("{}#{}", block.url, song_index);
let reader = Cursor::new(flac_data.clone());
let data_len = flac_data.len() as u64;
let reader = Cursor::new(flac_data);
let audio_pk: String = channel
.cache_manager
.cache_audio_from_reader(&audio_source_uri, reader, Some(flac_data.len() as u64))
.cache_audio_from_reader(&audio_source_uri, reader, Some(data_len))
.await?;
channel.cache_manager.wait_audio_ready(&audio_pk).await?;
let cached_cover_pk = if let Some(ref image_base) = block.image_base {
if let Some(ref cover) = song.cover {
let image_url = format!("{}{}", image_base, cover);
@@ -461,6 +473,19 @@ impl RadioParadiseSource {
None
};
let metadata_cover_pk = cached_cover_pk.clone();
channel
.cache_manager
.update_metadata(
track_id.clone(),
pmosource::TrackMetadata {
original_uri: block.url.clone(),
cached_audio_pk: Some(audio_pk.clone()),
cached_cover_pk: metadata_cover_pk,
},
)
.await;
let playback_url = channel.cache_manager.resolve_uri(&track_id).await?;
let mut track = Track::new(track_id.clone(), song.title.clone(), playback_url);
@@ -487,19 +512,33 @@ impl RadioParadiseSource {
}
}
channel
.cache_manager
.update_metadata(
track_id.clone(),
pmosource::TrackMetadata {
original_uri: block.url.clone(),
cached_audio_pk: Some(audio_pk.clone()),
cached_cover_pk,
},
)
.await;
channel.playlist.append_track(track).await;
let channel_for_wait = channel.clone();
let track_id_for_wait = track_id.clone();
let audio_pk_for_wait = audio_pk.clone();
tokio::spawn(async move {
if let Err(e) = channel_for_wait
.cache_manager
.wait_audio_ready(&audio_pk_for_wait)
.await
{
tracing::error!(
"Failed to finalize audio {} on channel {}: {}",
track_id_for_wait,
channel_for_wait.descriptor.name,
e
);
channel_for_wait
.cache_manager
.remove_track(&track_id_for_wait)
.await;
channel_for_wait
.playlist
.remove_by_id(&track_id_for_wait)
.await;
}
});
}
tracing::info!(