Changes: 1. Updated pmoserver_ext.rs to use channels module: - Changed imports from paradise:: to channels:: - Now uses ChannelDescriptor from channels module - No longer depends on paradise orchestration code 2. Updated lib.rs: - Added pub mod channels - Maintains existing modules for now (will evaluate removal later) 3. Verification: - All tests pass (25/25) - No dead code warnings - Compilation successful with all features The REST API is now simplified to provide only direct Radio Paradise API access without the heavy orchestration layer.
pmoparadise
An idiomatic Rust client library for Radio Paradise streaming service.
Features
- 🎵 Metadata Access - Fetch 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 lossless
- 🎼 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
- 📚 Well Documented - Extensive API documentation and examples
Installation
Add to your Cargo.toml:
[dependencies]
pmoparadise = "0.1.0"
For per-track extraction support:
[dependencies]
pmoparadise = { version = "0.1.0", features = ["per-track"] }
Quick Start
use pmoparadise::RadioParadiseClient;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a client
let client = RadioParadiseClient::new().await?;
// Get what's currently playing
let now_playing = client.now_playing().await?;
if let Some(song) = &now_playing.current_song {
println!("Now Playing: {} - {}", song.artist, song.title);
println!("Album: {}", song.album);
}
Ok(())
}
Usage Examples
Display Current Block Information
use pmoparadise::RadioParadiseClient;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = RadioParadiseClient::new().await?;
let block = client.get_block(None).await?;
println!("Block {} contains {} songs", block.event, block.song_count());
for (index, song) in block.songs_ordered() {
println!("{}. {} - {} ({}s)",
index + 1,
song.artist,
song.title,
song.duration / 1000);
}
Ok(())
}
Stream a Block
use pmoparadise::RadioParadiseClient;
use futures::StreamExt;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = RadioParadiseClient::new().await?;
let block = client.get_block(None).await?;
let mut stream = client.stream_block_from_metadata(&block).await?;
while let Some(chunk) = stream.next().await {
let bytes = chunk?;
// Feed to audio player, write to file, etc.
println!("Received {} bytes", bytes.len());
}
Ok(())
}
Configure Quality Level
use pmoparadise::{RadioParadiseClient, Bitrate};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = RadioParadiseClient::builder()
.bitrate(Bitrate::Aac320) // Use AAC 320 kbps
.build()
.await?;
Ok(())
}
Continuous Playback with Prefetching
use pmoparadise::RadioParadiseClient;
use futures::StreamExt;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = RadioParadiseClient::new().await?;
let mut current_block = client.get_block(None).await?;
loop {
println!("Playing block {}", current_block.event);
// Prefetch next block
client.prefetch_next(¤t_block).await?;
// Stream current block
let mut stream = client.stream_block_from_metadata(¤t_block).await?;
while let Some(chunk) = stream.next().await {
let bytes = chunk?;
// Send to audio player
}
// Move to next block
current_block = client.get_block(Some(current_block.end_event)).await?;
}
}
Quality Levels
Radio Paradise offers 5 quality levels via the Bitrate enum:
| Bitrate | Format | Description |
|---|---|---|
Mp3_128 |
MP3 | 128 kbps MP3 |
Aac64 |
AAC | 64 kbps AAC |
Aac128 |
AAC | 128 kbps AAC |
Aac320 |
AAC | 320 kbps AAC |
Flac |
FLAC | Lossless (default) |
Per-Track Extraction
⚠️ Important: This feature has significant tradeoffs. See details below.
The Problem
Radio Paradise publishes blocks containing multiple songs, not individual per-track files. Each block is a single FLAC or AAC file with metadata indicating timing offsets for each song.
Block URL pattern:
https://apps.radioparadise.com/blocks/chan/0/4/<start_event>-<end_event>.flac
The song[i].elapsed field (in milliseconds) indicates when each track starts within the block.
Recommended Approach: Player-Based Seeking
For most use cases, let your audio player handle seeking:
# Play a specific track using mpv
mpv --start=123.5 --length=234.0 <block_url>
# Extract a track using ffmpeg
ffmpeg -ss 123.5 -t 234.0 -i <block_url> -c copy track.flac
Get timing information from the API:
let client = RadioParadiseClient::new().await?;
let block = client.get_block(None).await?;
let (start_sec, duration_sec) = client.track_position_seconds(&block, 0)?;
println!("mpv --start={} --length={} {}", start_sec, duration_sec, block.url);
Benefits of player-based seeking:
- ✅ No need to download entire block
- ✅ Uses player's optimized seeking
- ✅ Starts playback immediately
- ✅ Preserves original quality
- ✅ Minimal CPU usage
Alternative: FLAC Decoding (Feature: per-track)
If you need PCM samples or WAV files for processing:
use pmoparadise::RadioParadiseClient;
use std::path::Path;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = RadioParadiseClient::new().await?;
let block = client.get_block(None).await?;
// Extract first track to WAV
let mut track = client.open_track_stream(&block, 0).await?;
track.export_wav(Path::new("track.wav"))?;
Ok(())
}
Tradeoffs:
- ❌ Downloads entire block (50-100 MB) to temporary file
- ❌ High latency before playback can start
- ❌ CPU-intensive FLAC decoding
- ❌ FLAC doesn't support random access (must decode from beginning)
When to use:
- You need individual WAV files for further processing
- You need raw PCM data for custom audio analysis
- You need separate files for non-streaming scenarios
Radio Paradise Block Format
Understanding the block format is essential for working with Radio Paradise:
Block Structure
- Each block is a single audio file (FLAC or AAC)
- Blocks contain multiple songs (typically 10-15 minutes total)
- Metadata includes timing offsets for each song (
song[i].elapsedin ms) - Blocks are seamlessly chained:
block_n.end_event == block_n+1.event
Block Metadata Example
{
"event": 1234,
"end_event": 5678,
"length": 900000,
"url": "https://apps.radioparadise.com/blocks/chan/0/4/1234-5678.flac",
"image_base": "https://img.radioparadise.com/covers/l/",
"song": {
"0": {
"artist": "Miles Davis",
"title": "So What",
"album": "Kind of Blue",
"year": 1959,
"elapsed": 0,
"duration": 540000,
"cover": "B00000I0JF.jpg"
},
"1": {
"artist": "John Coltrane",
"title": "Giant Steps",
"album": "Giant Steps",
"year": 1960,
"elapsed": 540000,
"duration": 360000,
"cover": "B000002I4U.jpg"
}
}
}
Timing Information
event: Start event ID for this blockend_event: End event ID (= start of next block)length: Total duration in millisecondssong[i].elapsed: Start time of songiin millisecondssong[i].duration: Duration of songiin milliseconds
Best Practices
For Continuous Playback
- Fetch current block with
get_block(None) - Start streaming the block
- Call
prefetch_next()early (before block ends) - When block finishes, seamlessly transition to next block
- Repeat
For Gapless Playback
- Use the
end_eventto fetch the next block - Prefetch metadata and prepare the stream before the current block ends
- Modern audio players (mpv, VLC) handle gapless FLAC natively
For User Controls (Skip Track)
Recommended: Stream entire block to player, use player's seek commands:
let (start, duration) = client.track_position_seconds(&block, track_index)?;
// Send seek command to player
Alternative: Re-stream from a different block or position
Network Best Practices
- Set appropriate User-Agent:
RadioParadiseClient::builder().user_agent("MyApp/1.0") - Implement retry logic with exponential backoff
- Respect Radio Paradise's infrastructure (no excessive parallel streams)
- Cache block metadata locally to reduce API calls
Error Handling
All operations return Result<T, Error> with detailed error types:
use pmoparadise::{RadioParadiseClient, Error};
match client.get_block(Some(12345)).await {
Ok(block) => println!("Got block: {}", block.event),
Err(Error::Http(e)) => eprintln!("Network error: {}", e),
Err(Error::Json(e)) => eprintln!("Parse error: {}", e),
Err(Error::InvalidEvent(e)) => eprintln!("Invalid event: {}", e),
Err(e) => eprintln!("Other error: {}", e),
}
Available error types:
Http- Network/HTTP errorsJson- JSON parsing errorsInvalidUrl- URL parsing errorsIo- File I/O errorsInvalidIndex- Invalid track indexInvalidBitrate- Invalid quality levelInvalidEvent- Invalid event IDFlacDecode- FLAC decoding errors (per-track feature)WavEncode- WAV encoding errors (per-track feature)Timeout- Request timeoutOther- Generic errors
Cargo Features
default = ["metadata-only"]- Standard metadata and streaming (no FLAC decoding)per-track- Enable FLAC decoding and per-track extraction (adds dependencies:claxon,hound,tempfile)logging- Enable tracing logs for debugging
Examples
Run examples with:
# Display current block and songs
cargo run --example now_playing
# Stream a block to stdout (pipe to player)
cargo run --example stream_block | mpv -
# Extract individual tracks (requires per-track feature)
cargo run --example extract_track --features per-track
Architecture
pmoparadise/
├── src/
│ ├── lib.rs # Library root and documentation
│ ├── client.rs # HTTP client and API methods
│ ├── models.rs # Data structures (Block, Song, etc.)
│ ├── stream.rs # Block streaming functionality
│ ├── track.rs # Per-track extraction (feature-gated)
│ └── error.rs # Error types
├── examples/ # Usage examples
│ ├── now_playing.rs
│ ├── stream_block.rs
│ └── extract_track.rs
└── tests/ # Integration tests
└── integration_tests.rs
Testing
# Run all tests (metadata-only)
cargo test
# Run tests with per-track feature
cargo test --features per-track
# Run integration tests
cargo test --test integration_tests
# Run with logging
RUST_LOG=debug cargo test
Requirements
- Rust 1.90+ (2021 edition)
- Tokio async runtime
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Disclaimer
This library is not affiliated with or endorsed by Radio Paradise. Please respect their Terms of Service when using this library.
Credits
Inspired by the Radio Paradise API and the Python implementation in upmpdcli.
See Also
- Radio Paradise - Official website
- Radio Paradise API Documentation
- PMOMusic - Parent project