2025-10-16 22:13:00 +02:00
# pmosource - Music Source Abstraction for PMOMusic
2025-10-16 22:00:35 +02:00
Common traits and types for PMOMusic sources.
2025-10-16 22:13:00 +02:00
This crate provides the foundational abstractions for different music sources in the PMOMusic ecosystem, such as Radio Paradise, Qobuz, local playlists, etc.
2025-10-16 22:00:35 +02:00
## Features
2025-10-16 22:13:00 +02:00
- **FIFO Support**: Dynamic audio sources using `pmoplaylist` for streaming
- **Container/Item Navigation**: Browse and search using DIDL-Lite format (`pmodidl` )
- **Cache Integration**: Automatic URI resolution with `pmoaudiocache` and `pmocovers`
- **Change Tracking**: `update_id` and `last_change` for UPnP notifications
- **Send + Sync**: Ready for async servers
2025-10-16 22:00:35 +02:00
2025-10-16 22:13:00 +02:00
## Architecture
2025-10-16 22:00:35 +02:00
2025-10-16 22:13:00 +02:00
The `MusicSource` trait provides a unified interface for all music sources:
2025-10-16 22:00:35 +02:00
2025-10-16 22:13:00 +02:00
```
┌─────────────────────────────────────┐
│ MusicSource Trait │
├─────────────────────────────────────┤
│ • Basic Info (name, id, image) │
│ • ContentDirectory (browse, search) │
│ • URI Resolution (with caching) │
│ • FIFO Management │
│ • Change Tracking │
└─────────────────────────────────────┘
▲ ▲ ▲
│ │ │
┌────┴───┐ ┌──┴────┐ ┌──┴─────┐
│ Radio │ │ Qobuz │ │ Local │
│Paradise│ │ │ │Playlist│
└────────┘ └───────┘ └────────┘
```
2025-10-16 22:00:35 +02:00
2025-10-16 22:13:00 +02:00
## Quick Start
### Implementing a Music Source
```rust
use pmosource::{async_trait, MusicSource, BrowseResult, Result};
use pmodidl::{Container, Item};
use pmoplaylist::FifoPlaylist;
use std::time::SystemTime;
2025-10-16 22:00:35 +02:00
#[derive(Debug)]
2025-10-16 22:13:00 +02:00
pub struct MyRadioSource {
playlist: FifoPlaylist,
// ... other fields
}
2025-10-16 22:00:35 +02:00
2025-10-16 22:13:00 +02:00
#[async_trait]
impl MusicSource for MyRadioSource {
2025-10-16 22:00:35 +02:00
fn name(&self) -> &str {
2025-10-16 22:13:00 +02:00
"My Radio"
2025-10-16 22:00:35 +02:00
}
fn id(&self) -> &str {
2025-10-16 22:13:00 +02:00
"my-radio"
2025-10-16 22:00:35 +02:00
}
fn default_image(&self) -> &[u8] {
2025-10-16 22:13:00 +02:00
include_bytes!("../assets/my-radio.webp")
}
async fn root_container(&self) -> Result<Container> {
Ok(self.playlist.as_container().await)
}
async fn browse(&self, object_id: &str) -> Result<BrowseResult> {
// Return items from FIFO
let tracks = self.playlist.get_items(0, 100).await;
// Convert tracks to Items...
Ok(BrowseResult::Items(items))
}
async fn resolve_uri(&self, object_id: &str) -> Result<String> {
// Return cached URI if available, or original URI
Ok(format!("http://cache-server/audio/{}", object_id))
}
fn supports_fifo(&self) -> bool {
true
}
async fn append_track(&self, track: Item) -> Result<()> {
// Convert Item to Track and add to playlist
self.playlist.append_track(pmo_track).await;
Ok(())
}
async fn remove_oldest(&self) -> Result<Option<Item>> {
if let Some(track) = self.playlist.remove_oldest().await {
// Convert Track to Item and return
Ok(Some(item))
} else {
Ok(None)
}
}
async fn update_id(&self) -> u32 {
self.playlist.update_id().await
}
async fn last_change(&self) -> Option<SystemTime> {
Some(self.playlist.last_change().await)
}
async fn get_items(&self, offset: usize, count: usize) -> Result<Vec<Item>> {
let tracks = self.playlist.get_items(offset, count).await;
// Convert tracks to Items...
Ok(items)
2025-10-16 22:00:35 +02:00
}
}
```
2025-10-16 22:13:00 +02:00
### Using a Music Source
2025-10-16 22:00:35 +02:00
```rust
use pmosource::MusicSource;
2025-10-16 22:13:00 +02:00
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let source = MyRadioSource::new("http://localhost:8080");
// Get source info
println!("Source: {}", source.name());
println!("ID: {}", source.id());
// Get root container for ContentDirectory
let root = source.root_container().await?;
println!("Root: {} ({})", root.title, root.id);
// Browse items
let result = source.browse(&root.id).await?;
for item in result.items() {
println!("Track: {}", item.title);
}
// Resolve audio URI
let uri = source.resolve_uri("track-123").await?;
println!("Stream from: {}", uri);
// Track changes
println!("Update ID: {}", source.update_id().await);
Ok(())
}
```
## Trait Methods
### Basic Information
- `name() -> &str` : Human-readable name
- `id() -> &str` : Unique identifier (e.g., "radio-paradise")
- `default_image() -> &[u8]` : Embedded WebP logo (300x300px)
- `default_image_mime_type() -> &str` : MIME type (default: "image/webp")
2025-10-16 22:00:35 +02:00
2025-10-16 22:13:00 +02:00
### ContentDirectory Navigation
- `root_container() -> Container` : Root container for UPnP ContentDirectory
- `browse(object_id: &str) -> BrowseResult` : Browse containers/items
- `resolve_uri(object_id: &str) -> String` : Get audio URI (cached or original)
### FIFO Support (Dynamic Sources)
- `supports_fifo() -> bool` : Whether this source uses a FIFO
- `append_track(track: Item)` : Add track to FIFO (auto-removes oldest if full)
- `remove_oldest() -> Option<Item>` : Remove oldest track from FIFO
### Change Tracking
- `update_id() -> u32` : Increments on each change (for UPnP notifications)
- `last_change() -> Option<SystemTime>` : Timestamp of last modification
### Pagination & Search
- `get_items(offset: usize, count: usize) -> Vec<Item>` : Paginated browsing
- `search(query: &str) -> BrowseResult` : Search (optional, default: not supported)
## Integration with PMOMusic Ecosystem
### With pmoplaylist
Sources that support FIFO (radios, streaming services) use `pmoplaylist::FifoPlaylist` to manage dynamic track lists:
```rust
use pmoplaylist::{FifoPlaylist, Track};
let playlist = FifoPlaylist::new(
"my-radio".to_string(),
"My Radio".to_string(),
50, // capacity
DEFAULT_IMAGE,
);
// Add tracks
playlist.append_track(Track::new("id", "title", "uri")).await;
// Tracks automatically removed when capacity reached
2025-10-16 22:00:35 +02:00
```
2025-10-16 22:13:00 +02:00
### With pmoaudiocache
2025-10-16 22:00:35 +02:00
2025-10-16 22:13:00 +02:00
When the `cache` feature is enabled, sources can integrate with `pmoaudiocache` to:
- Cache audio files locally (with FLAC conversion)
- Serve from local cache instead of re-streaming
- Extract and merge metadata
```rust
// Resolve URI checks cache first
async fn resolve_uri(&self, object_id: &str) -> Result<String> {
if let Some(cached_pk) = self.get_cached_pk(object_id).await {
Ok(format!("{}/audio/cache/{}", self.cache_base_url, cached_pk))
} else {
Ok(self.get_original_uri(object_id))
}
}
```
2025-10-16 22:00:35 +02:00
2025-10-16 22:13:00 +02:00
### With pmocovers
2025-10-16 22:00:35 +02:00
2025-10-16 22:13:00 +02:00
When the `cache` feature is enabled, sources can integrate with `pmocovers` to:
- Cache album art locally (with WebP conversion)
- Generate multiple size variants
- Serve optimized images
2025-10-16 22:00:35 +02:00
2025-10-16 22:13:00 +02:00
### With pmodidl
2025-10-16 22:00:35 +02:00
2025-10-16 22:13:00 +02:00
All sources use `pmodidl` for DIDL-Lite generation compatible with UPnP/DLNA.
2025-10-16 22:00:35 +02:00
2025-10-16 22:13:00 +02:00
## Examples
2025-10-16 22:00:35 +02:00
2025-10-16 22:13:00 +02:00
### Radio Paradise
2025-10-16 22:00:35 +02:00
2025-10-16 22:13:00 +02:00
See [examples/radio_paradise.rs ](examples/radio_paradise.rs ) for a complete implementation of a streaming radio source with:
- FIFO management using `pmoplaylist`
- Simulated cache integration
- Full DIDL-Lite export
- Change tracking
2025-10-16 22:00:35 +02:00
2025-10-16 22:13:00 +02:00
Run the example:
```bash
cargo run --example radio_paradise
2025-10-16 22:00:35 +02:00
```
2025-10-16 22:13:00 +02:00
## Design Patterns
### Static Sources (Albums, Local Playlists)
```rust
impl MusicSource for LocalAlbum {
fn supports_fifo(&self) -> bool {
false // Static content
}
async fn append_track(&self, _: Item) -> Result<()> {
Err(MusicSourceError::FifoNotSupported)
}
2025-10-16 22:00:35 +02:00
2025-10-16 22:13:00 +02:00
async fn update_id(&self) -> u32 {
0 // Never changes
}
}
```
2025-10-16 22:00:35 +02:00
2025-10-16 22:13:00 +02:00
### Dynamic Sources (Radios, Streaming Services)
```rust
impl MusicSource for RadioSource {
fn supports_fifo(&self) -> bool {
true // Dynamic content
}
async fn append_track(&self, track: Item) -> Result<()> {
// Add to pmoplaylist::FifoPlaylist
self.playlist.append_track(converted_track).await;
Ok(())
}
async fn update_id(&self) -> u32 {
self.playlist.update_id().await
}
}
```
2025-10-16 22:00:35 +02:00
2025-10-16 22:13:00 +02:00
## Thread Safety
2025-10-16 22:00:35 +02:00
2025-10-16 22:13:00 +02:00
All `MusicSource` implementations must be `Send + Sync` for use in async servers.
2025-10-16 22:00:35 +02:00
## License
MIT OR Apache-2.0