retire le support des codec non flac de radio paradise
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
//! HTTP client for Radio Paradise API
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::models::{Bitrate, Block, EventId, NowPlaying};
|
||||
use crate::models::{Block, EventId, NowPlaying};
|
||||
use reqwest::Client;
|
||||
use std::time::Duration;
|
||||
use url::Url;
|
||||
@@ -50,7 +50,6 @@ pub struct RadioParadiseClient {
|
||||
pub(crate) client: Client,
|
||||
api_base: String,
|
||||
block_base: String,
|
||||
bitrate: Bitrate,
|
||||
channel: u8,
|
||||
pub(crate) request_timeout: Duration,
|
||||
pub(crate) block_timeout: Duration,
|
||||
@@ -60,7 +59,7 @@ pub struct RadioParadiseClient {
|
||||
impl RadioParadiseClient {
|
||||
/// Create a new client with default settings
|
||||
///
|
||||
/// Uses FLAC quality (bitrate 4) and channel 0 (main mix)
|
||||
/// Uses FLAC quality and channel 0 (main mix)
|
||||
pub async fn new() -> Result<Self> {
|
||||
Self::builder().build().await
|
||||
}
|
||||
@@ -78,7 +77,6 @@ impl RadioParadiseClient {
|
||||
client,
|
||||
api_base: DEFAULT_API_BASE.to_string(),
|
||||
block_base: DEFAULT_BLOCK_BASE.to_string(),
|
||||
bitrate: Bitrate::default(),
|
||||
channel: 0,
|
||||
request_timeout: Duration::from_secs(DEFAULT_REQUEST_TIMEOUT_SECS),
|
||||
block_timeout: Duration::from_secs(DEFAULT_BLOCK_TIMEOUT_SECS),
|
||||
@@ -86,11 +84,6 @@ impl RadioParadiseClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current bitrate setting
|
||||
pub fn bitrate(&self) -> Bitrate {
|
||||
self.bitrate
|
||||
}
|
||||
|
||||
/// Get the current channel (0 = main mix)
|
||||
pub fn channel(&self) -> u8 {
|
||||
self.channel
|
||||
@@ -109,13 +102,6 @@ impl RadioParadiseClient {
|
||||
cloned
|
||||
}
|
||||
|
||||
/// Clone the client with a different bitrate while preserving other settings.
|
||||
pub fn clone_with_bitrate(&self, bitrate: Bitrate) -> Self {
|
||||
let mut cloned = self.clone();
|
||||
cloned.bitrate = bitrate;
|
||||
cloned.next_block_url = None;
|
||||
cloned
|
||||
}
|
||||
|
||||
/// Get a block by event ID
|
||||
///
|
||||
@@ -147,7 +133,7 @@ impl RadioParadiseClient {
|
||||
let mut url = Url::parse(&format!("{}/get_block", self.api_base))?;
|
||||
|
||||
url.query_pairs_mut()
|
||||
.append_pair("bitrate", &self.bitrate.as_u8().to_string())
|
||||
.append_pair("bitrate", "4") // FLAC lossless
|
||||
.append_pair("info", "true")
|
||||
.append_pair("channel", &self.channel.to_string());
|
||||
|
||||
@@ -251,7 +237,6 @@ pub struct ClientBuilder {
|
||||
client: Option<Client>,
|
||||
api_base: String,
|
||||
block_base: String,
|
||||
bitrate: Bitrate,
|
||||
channel: u8,
|
||||
request_timeout: Duration,
|
||||
block_timeout: Duration,
|
||||
@@ -265,7 +250,6 @@ impl Default for ClientBuilder {
|
||||
client: None,
|
||||
api_base: DEFAULT_API_BASE.to_string(),
|
||||
block_base: DEFAULT_BLOCK_BASE.to_string(),
|
||||
bitrate: Bitrate::default(),
|
||||
channel: 0,
|
||||
request_timeout: Duration::from_secs(DEFAULT_REQUEST_TIMEOUT_SECS),
|
||||
block_timeout: Duration::from_secs(DEFAULT_BLOCK_TIMEOUT_SECS),
|
||||
@@ -299,19 +283,6 @@ impl ClientBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the bitrate/quality level
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use pmoparadise::{RadioParadiseClient, Bitrate};
|
||||
/// let builder = RadioParadiseClient::builder()
|
||||
/// .bitrate(Bitrate::Aac320);
|
||||
/// ```
|
||||
pub fn bitrate(mut self, bitrate: Bitrate) -> Self {
|
||||
self.bitrate = bitrate;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the channel (0 = main mix, 1 = mellow, 2 = rock, 3 = world/etc)
|
||||
pub fn channel(mut self, channel: u8) -> Self {
|
||||
@@ -371,7 +342,6 @@ impl ClientBuilder {
|
||||
client,
|
||||
api_base: self.api_base,
|
||||
block_base,
|
||||
bitrate: self.bitrate,
|
||||
channel: self.channel,
|
||||
request_timeout: self.request_timeout,
|
||||
block_timeout: self.block_timeout,
|
||||
@@ -388,7 +358,6 @@ mod tests {
|
||||
fn test_builder_defaults() {
|
||||
let builder = ClientBuilder::default();
|
||||
assert_eq!(builder.api_base, DEFAULT_API_BASE);
|
||||
assert_eq!(builder.bitrate, Bitrate::Flac);
|
||||
assert_eq!(builder.channel, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
//! ## Features
|
||||
//!
|
||||
//! - **Metadata Access**: Get current and historical block metadata with song information
|
||||
//! - **Block Streaming**: Stream continuous FLAC/AAC blocks with automatic prefetching
|
||||
//! - **Multiple Quality Levels**: Support for MP3, AAC (64/128/320 kbps), and FLAC
|
||||
//! - **Block Streaming**: Stream continuous FLAC blocks with automatic prefetching
|
||||
//! - **FLAC Quality**: Lossless CD quality or better
|
||||
//! - **Per-Track Extraction** (optional): Extract individual tracks from FLAC blocks
|
||||
//! - **Async/Await**: Built on tokio for efficient async I/O
|
||||
//! - **Type-Safe**: Strongly typed API with comprehensive error handling
|
||||
@@ -49,7 +49,7 @@
|
||||
//! ## Streaming Blocks
|
||||
//!
|
||||
//! Radio Paradise broadcasts music in continuous "blocks" - each block is a single
|
||||
//! FLAC or AAC file containing multiple songs with metadata indicating timing offsets.
|
||||
//! FLAC file containing multiple songs with metadata indicating timing offsets.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use pmoparadise::RadioParadiseClient;
|
||||
@@ -72,24 +72,6 @@
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Quality Levels
|
||||
//!
|
||||
//! Radio Paradise offers multiple quality levels via the [`Bitrate`] enum:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use pmoparadise::{RadioParadiseClient, Bitrate};
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let client = RadioParadiseClient::builder()
|
||||
//! .bitrate(Bitrate::Aac320)
|
||||
//! .build()
|
||||
//! .await?;
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Per-Track Extraction (Feature: `per-track`)
|
||||
//!
|
||||
//! **Important**: This is an advanced feature with significant tradeoffs.
|
||||
@@ -135,7 +117,7 @@
|
||||
//!
|
||||
//! Radio Paradise streams use a block-based format:
|
||||
//!
|
||||
//! - Each block is a single audio file (FLAC or AAC)
|
||||
//! - Each block is a single FLAC audio file
|
||||
//! - Blocks contain multiple songs (typically 10-15 minutes total)
|
||||
//! - Metadata includes timing offsets (`song[i].elapsed` in ms) for each song
|
||||
//! - Block URLs follow the pattern: `https://apps.radioparadise.com/blocks/chan/0/4/<start>-<end>.flac`
|
||||
@@ -256,7 +238,7 @@ pub mod pmoserver_ext;
|
||||
// Re-exports for convenience
|
||||
pub use client::{ClientBuilder, RadioParadiseClient};
|
||||
pub use error::{Error, Result};
|
||||
pub use models::{Bitrate, Block, DurationMs, EventId, NowPlaying, Song};
|
||||
pub use models::{Block, DurationMs, EventId, NowPlaying, Song};
|
||||
pub use source::RadioParadiseSource;
|
||||
pub use stream::BlockStream;
|
||||
|
||||
|
||||
@@ -31,12 +31,10 @@
|
||||
//! # #[cfg(feature = "mediaserver")]
|
||||
//! # {
|
||||
//! use pmoparadise::mediaserver::RadioParadiseMediaServer;
|
||||
//! use pmoparadise::Bitrate;
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let server = RadioParadiseMediaServer::new()
|
||||
//! .with_bitrate(Bitrate::Flac)
|
||||
//! let server = RadioParadiseMediaServer::builder()
|
||||
//! .with_friendly_name("Radio Paradise FLAC")
|
||||
//! .build()
|
||||
//! .await?;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Radio Paradise UPnP Media Server implementation
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::models::Bitrate;
|
||||
use crate::RadioParadiseClient;
|
||||
use pmoserver::Server;
|
||||
use pmoupnp::devices::Device;
|
||||
@@ -55,7 +54,6 @@ pub struct MediaServerBuilder {
|
||||
friendly_name: String,
|
||||
manufacturer: String,
|
||||
model_name: String,
|
||||
bitrate: Bitrate,
|
||||
channel: u8,
|
||||
port: u16,
|
||||
}
|
||||
@@ -66,7 +64,6 @@ impl Default for MediaServerBuilder {
|
||||
friendly_name: "Radio Paradise Media Server".to_string(),
|
||||
manufacturer: "PMOMusic".to_string(),
|
||||
model_name: "Radio Paradise Adapter".to_string(),
|
||||
bitrate: Bitrate::Flac,
|
||||
channel: 0,
|
||||
port: 8080,
|
||||
}
|
||||
@@ -97,12 +94,6 @@ impl MediaServerBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the bitrate/quality level
|
||||
pub fn with_bitrate(mut self, bitrate: Bitrate) -> Self {
|
||||
self.bitrate = bitrate;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Radio Paradise channel (0=main, 1=mellow, 2=rock, 3=world)
|
||||
pub fn with_channel(mut self, channel: u8) -> Self {
|
||||
self.channel = channel;
|
||||
@@ -119,7 +110,6 @@ impl MediaServerBuilder {
|
||||
pub async fn build(self) -> Result<RadioParadiseMediaServer> {
|
||||
// Create Radio Paradise client
|
||||
let client = RadioParadiseClient::builder()
|
||||
.bitrate(self.bitrate)
|
||||
.channel(self.channel)
|
||||
.build()
|
||||
.await?;
|
||||
@@ -180,7 +170,6 @@ mod tests {
|
||||
fn test_builder_defaults() {
|
||||
let builder = MediaServerBuilder::default();
|
||||
assert_eq!(builder.friendly_name, "Radio Paradise Media Server");
|
||||
assert_eq!(builder.bitrate, Bitrate::Flac);
|
||||
assert_eq!(builder.channel, 0);
|
||||
assert_eq!(builder.port, 8080);
|
||||
}
|
||||
@@ -189,12 +178,10 @@ mod tests {
|
||||
fn test_builder_customization() {
|
||||
let builder = MediaServerBuilder::new()
|
||||
.with_friendly_name("Custom Server")
|
||||
.with_bitrate(Bitrate::Aac320)
|
||||
.with_channel(1)
|
||||
.with_port(9090);
|
||||
|
||||
assert_eq!(builder.friendly_name, "Custom Server");
|
||||
assert_eq!(builder.bitrate, Bitrate::Aac320);
|
||||
assert_eq!(builder.channel, 1);
|
||||
assert_eq!(builder.port, 9090);
|
||||
}
|
||||
|
||||
@@ -122,65 +122,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Bitrate quality levels for Radio Paradise streams
|
||||
///
|
||||
/// Radio Paradise offers 5 quality levels:
|
||||
/// - 0: 128 kbps MP3
|
||||
/// - 1: AAC 64 kbps
|
||||
/// - 2: AAC 128 kbps
|
||||
/// - 3: AAC 320 kbps
|
||||
/// - 4: FLAC lossless (CD quality or better)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[repr(u8)]
|
||||
pub enum Bitrate {
|
||||
/// 128 kbps MP3
|
||||
Mp3_128 = 0,
|
||||
/// AAC 64 kbps
|
||||
Aac64 = 1,
|
||||
/// AAC 128 kbps
|
||||
Aac128 = 2,
|
||||
/// AAC 320 kbps
|
||||
Aac320 = 3,
|
||||
/// FLAC lossless
|
||||
Flac = 4,
|
||||
}
|
||||
|
||||
impl Bitrate {
|
||||
/// Convert from u8 value
|
||||
pub fn from_u8(value: u8) -> Result<Self, crate::error::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::Mp3_128),
|
||||
1 => Ok(Self::Aac64),
|
||||
2 => Ok(Self::Aac128),
|
||||
3 => Ok(Self::Aac320),
|
||||
4 => Ok(Self::Flac),
|
||||
_ => Err(crate::error::Error::InvalidBitrate(value)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to u8 value
|
||||
pub fn as_u8(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
|
||||
/// Get human-readable description
|
||||
pub fn description(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Mp3_128 => "MP3 128 kbps",
|
||||
Self::Aac64 => "AAC 64 kbps",
|
||||
Self::Aac128 => "AAC 128 kbps",
|
||||
Self::Aac320 => "AAC 320 kbps",
|
||||
Self::Flac => "FLAC Lossless",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Bitrate {
|
||||
fn default() -> Self {
|
||||
Self::Flac
|
||||
}
|
||||
}
|
||||
|
||||
/// Duration in milliseconds
|
||||
pub type DurationMs = u64;
|
||||
|
||||
@@ -376,13 +317,6 @@ impl NowPlaying {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_bitrate_conversion() {
|
||||
assert_eq!(Bitrate::from_u8(0).unwrap(), Bitrate::Mp3_128);
|
||||
assert_eq!(Bitrate::from_u8(4).unwrap(), Bitrate::Flac);
|
||||
assert!(Bitrate::from_u8(5).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_song_timing() {
|
||||
let song = Song {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! à un serveur pmoserver.
|
||||
|
||||
use crate::paradise::{max_channel_id, ParadiseChannel, PlaylistEntry, ALL_CHANNELS};
|
||||
use crate::{models::Bitrate, Block, NowPlaying, RadioParadiseClient, RadioParadiseSource};
|
||||
use crate::{Block, NowPlaying, RadioParadiseClient, RadioParadiseSource};
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{Path, Query, State},
|
||||
@@ -34,7 +34,6 @@ pub struct RadioParadiseState {
|
||||
#[serde(default)]
|
||||
struct ParadiseQuery {
|
||||
channel: Option<u8>,
|
||||
bitrate: Option<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
@@ -106,14 +105,6 @@ impl RadioParadiseState {
|
||||
client = client.clone_with_channel(channel);
|
||||
}
|
||||
|
||||
if let Some(bitrate_id) = params.bitrate {
|
||||
let bitrate = Bitrate::from_u8(bitrate_id).map_err(|e| {
|
||||
tracing::warn!("Invalid Radio Paradise bitrate requested: {}", e);
|
||||
StatusCode::BAD_REQUEST
|
||||
})?;
|
||||
client = client.clone_with_bitrate(bitrate);
|
||||
}
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
@@ -288,8 +279,7 @@ impl From<NowPlaying> for NowPlayingResponse {
|
||||
get,
|
||||
path = "/now-playing",
|
||||
params(
|
||||
("channel" = Option<u8>, Query, description = "Channel ID (0-3)"),
|
||||
("bitrate" = Option<u8>, Query, description = "Bitrate ID (0-4)")
|
||||
("channel" = Option<u8>, Query, description = "Channel ID (0-3)")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Morceau en cours", body = NowPlayingResponse),
|
||||
@@ -315,8 +305,7 @@ async fn get_now_playing(
|
||||
get,
|
||||
path = "/block/current",
|
||||
params(
|
||||
("channel" = Option<u8>, Query, description = "Channel ID (0-3)"),
|
||||
("bitrate" = Option<u8>, Query, description = "Bitrate ID (0-4)")
|
||||
("channel" = Option<u8>, Query, description = "Channel ID (0-3)")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Block actuel", body = BlockResponse),
|
||||
@@ -343,8 +332,7 @@ async fn get_current_block(
|
||||
path = "/block/{event_id}",
|
||||
params(
|
||||
("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)")
|
||||
("channel" = Option<u8>, Query, description = "Channel ID (0-3)")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Block demandé", body = BlockResponse),
|
||||
@@ -384,15 +372,6 @@ async fn get_channels() -> Json<Vec<ChannelInfo>> {
|
||||
Json(channels)
|
||||
}
|
||||
|
||||
/// Information sur un bitrate
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct BitrateInfo {
|
||||
/// ID du bitrate (0-4)
|
||||
pub id: u8,
|
||||
/// Nom/description
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Statut opérationnel d'un canal Radio Paradise
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct ChannelStatusResponse {
|
||||
@@ -532,42 +511,6 @@ pub struct ChannelHistoryResponse {
|
||||
pub entries: Vec<ChannelHistoryEntry>,
|
||||
}
|
||||
|
||||
/// GET /bitrates - Liste les bitrates disponibles
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/bitrates",
|
||||
responses(
|
||||
(status = 200, description = "Liste des bitrates disponibles", body = Vec<BitrateInfo>)
|
||||
),
|
||||
tag = "Radio Paradise"
|
||||
)]
|
||||
async fn get_bitrates() -> Json<Vec<BitrateInfo>> {
|
||||
let bitrates = vec![
|
||||
BitrateInfo {
|
||||
id: 0,
|
||||
name: "MP3 128 kbps".to_string(),
|
||||
},
|
||||
BitrateInfo {
|
||||
id: 1,
|
||||
name: "AAC 64 kbps".to_string(),
|
||||
},
|
||||
BitrateInfo {
|
||||
id: 2,
|
||||
name: "AAC 128 kbps".to_string(),
|
||||
},
|
||||
BitrateInfo {
|
||||
id: 3,
|
||||
name: "AAC 320 kbps".to_string(),
|
||||
},
|
||||
BitrateInfo {
|
||||
id: 4,
|
||||
name: "FLAC Lossless".to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
Json(bitrates)
|
||||
}
|
||||
|
||||
/// GET /channels/{channel_id}/status - Statut détaillé d'un canal
|
||||
#[utoipa::path(
|
||||
get,
|
||||
@@ -840,7 +783,6 @@ async fn stream_channel(
|
||||
get_channel_status,
|
||||
get_channel_playlist,
|
||||
get_channel_history,
|
||||
get_bitrates,
|
||||
stream_channel
|
||||
),
|
||||
components(schemas(
|
||||
@@ -848,7 +790,6 @@ async fn stream_channel(
|
||||
BlockResponse,
|
||||
SongInfo,
|
||||
ChannelInfo,
|
||||
BitrateInfo,
|
||||
ChannelStatusResponse,
|
||||
ChannelPlaylistEntry,
|
||||
ChannelPlaylistResponse,
|
||||
@@ -872,7 +813,6 @@ pub fn create_api_router(state: RadioParadiseState) -> Router {
|
||||
.route("/channels/{channel_id}/status", get(get_channel_status))
|
||||
.route("/channels/{channel_id}/playlist", get(get_channel_playlist))
|
||||
.route("/channels/{channel_id}/history", get(get_channel_history))
|
||||
.route("/bitrates", get(get_bitrates))
|
||||
.route("/stream", get(stream_channel))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user