# pmosource - Music Source Abstraction for PMOMusic Common traits and types for PMOMusic sources. This crate provides the foundational abstractions for different music sources in the PMOMusic ecosystem, such as Radio Paradise, Qobuz, local playlists, etc. ## Features - **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 ## Architecture The `MusicSource` trait provides a unified interface for all music sources: ``` ┌─────────────────────────────────────┐ │ MusicSource Trait │ ├─────────────────────────────────────┤ │ • Basic Info (name, id, image) │ │ • ContentDirectory (browse, search) │ │ • URI Resolution (with caching) │ │ • FIFO Management │ │ • Change Tracking │ └─────────────────────────────────────┘ ▲ ▲ ▲ │ │ │ ┌────┴───┐ ┌──┴────┐ ┌──┴─────┐ │ Radio │ │ Qobuz │ │ Local │ │Paradise│ │ │ │Playlist│ └────────┘ └───────┘ └────────┘ ``` ## 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; #[derive(Debug)] pub struct MyRadioSource { playlist: FifoPlaylist, // ... other fields } #[async_trait] impl MusicSource for MyRadioSource { fn name(&self) -> &str { "My Radio" } fn id(&self) -> &str { "my-radio" } fn default_image(&self) -> &[u8] { include_bytes!("../assets/my-radio.webp") } async fn root_container(&self) -> Result { Ok(self.playlist.as_container().await) } async fn browse(&self, object_id: &str) -> Result { // 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 { // 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> { 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 { Some(self.playlist.last_change().await) } async fn get_items(&self, offset: usize, count: usize) -> Result> { let tracks = self.playlist.get_items(offset, count).await; // Convert tracks to Items... Ok(items) } } ``` ### Using a Music Source ```rust use pmosource::MusicSource; #[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") ### 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`: Remove oldest track from FIFO ### Change Tracking - `update_id() -> u32`: Increments on each change (for UPnP notifications) - `last_change() -> Option`: Timestamp of last modification ### Pagination & Search - `get_items(offset: usize, count: usize) -> Vec`: 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 ``` ### With pmoaudiocache 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 { 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)) } } ``` ### With pmocovers 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 ### With pmodidl All sources use `pmodidl` for DIDL-Lite generation compatible with UPnP/DLNA. ## Examples ### Radio Paradise 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 Run the example: ```bash cargo run --example radio_paradise ``` ## 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) } async fn update_id(&self) -> u32 { 0 // Never changes } } ``` ### 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 } } ``` ## Thread Safety All `MusicSource` implementations must be `Send + Sync` for use in async servers. ## License MIT OR Apache-2.0