Files
pmomusic/pmosource/examples
..
2025-10-17 07:47:39 +02:00
2025-10-17 07:47:39 +02:00

PMOSource Examples

This directory contains example implementations of the MusicSource trait.

Available Examples

radio_paradise.rs

A complete implementation of a streaming radio source demonstrating:

  • FIFO Management: Using pmoplaylist::FifoPlaylist for dynamic track management
  • Cache Integration: Simulated integration with pmoaudiocache and pmocovers
  • DIDL-Lite Export: Proper conversion between pmoplaylist::Track and pmodidl::Item
  • Change Tracking: update_id and last_change for UPnP notifications
  • URI Resolution: Dynamic URI resolution with cache support
  • Pagination: Efficient browsing with get_items(offset, count)

Running the Example

cargo run --example radio_paradise

Expected Output

Radio Paradise Source Example
==============================

Source: Radio Paradise
ID: radio-paradise
Supports FIFO: true
Default image size: 9774 bytes

Adding sample tracks...
Added 3 tracks

Root Container:
  ID: radio-paradise
  Title: Radio Paradise
  Child Count: Some("3")

Browsing tracks:
  - Wish You Were Here by Pink Floyd (Wish You Were Here)
  - Bohemian Rhapsody by Queen (A Night at the Opera)
  - Hotel California by Eagles (Hotel California)

Resolving URIs:
  rp-001: http://stream.radioparadise.com/rp-001.mp3
  rp-002: http://stream.radioparadise.com/rp-002.mp3
  rp-003: http://stream.radioparadise.com/rp-003.mp3

Change Tracking:
  Update ID: 3
  Last Change: SystemTime { ... }

Simulating cache for rp-001...
  Cached URI: http://localhost:8080/audio/cache/cached-abc123

Pagination (get items 1-2):
  - Bohemian Rhapsody
  - Hotel California

Removing oldest track...
  Removed: Wish You Were Here
  New Update ID: 4

Browsing after removal:
  Tracks remaining: 2
    - Bohemian Rhapsody
    - Hotel California

Creating Your Own Source

1. Define the Source Structure

use pmosource::{async_trait, MusicSource, BrowseResult, Result};
use pmodidl::{Container, Item};
use pmoplaylist::FifoPlaylist;
use std::sync::Arc;
use tokio::sync::RwLock;

#[derive(Clone)]
pub struct MySource {
    inner: Arc<MySourceInner>,
}

struct MySourceInner {
    // For dynamic sources:
    playlist: FifoPlaylist,

    // For static sources:
    // items: Vec<Item>,

    // Other fields as needed
}

2. Implement Basic Information

#[async_trait]
impl MusicSource for MySource {
    fn name(&self) -> &str {
        "My Source Name"
    }

    fn id(&self) -> &str {
        "my-source"
    }

    fn default_image(&self) -> &[u8] {
        include_bytes!("../assets/my-source.webp")
    }
}

3. Implement ContentDirectory Methods

    async fn root_container(&self) -> Result<Container> {
        Ok(Container {
            id: self.id().to_string(),
            parent_id: "0".to_string(),
            title: self.name().to_string(),
            class: "object.container.playlistContainer".to_string(),
            child_count: Some("0".to_string()),
            containers: vec![],
            items: vec![],
        })
    }

    async fn browse(&self, object_id: &str) -> Result<BrowseResult> {
        // Return items for this container
        Ok(BrowseResult::Items(vec![]))
    }

    async fn resolve_uri(&self, object_id: &str) -> Result<String> {
        // Return URI for track
        Ok(format!("http://example.com/{}", object_id))
    }

4. Implement FIFO Methods (if applicable)

    fn supports_fifo(&self) -> bool {
        true  // or false for static sources
    }

    async fn append_track(&self, track: Item) -> Result<()> {
        // For dynamic sources: convert and add to playlist
        // For static sources: return FifoNotSupported error
        Ok(())
    }

    async fn remove_oldest(&self) -> Result<Option<Item>> {
        // For dynamic sources: remove from playlist
        // For static sources: return FifoNotSupported error
        Ok(None)
    }

5. Implement Change Tracking

    async fn update_id(&self) -> u32 {
        // For dynamic sources: delegate to playlist
        // For static sources: return 0
        0
    }

    async fn last_change(&self) -> Option<std::time::SystemTime> {
        // Return timestamp of last modification
        None
    }

6. Implement Pagination

    async fn get_items(&self, offset: usize, count: usize) -> Result<Vec<Item>> {
        // Return paginated items
        Ok(vec![])
    }

7. Implement Search (optional)

    async fn search(&self, query: &str) -> Result<BrowseResult> {
        // If search is not supported:
        Err(pmosource::MusicSourceError::SearchNotSupported)

        // If search is supported:
        // let results = self.search_items(query)?;
        // Ok(BrowseResult::Items(results))
    }

Best Practices

Thread Safety

Always use Arc<RwLock<>> for mutable state:

use std::sync::Arc;
use tokio::sync::RwLock;

struct MySourceInner {
    state: RwLock<HashMap<String, Track>>,
}

Error Handling

Use appropriate error types:

if object_id_not_found {
    return Err(MusicSourceError::ObjectNotFound(object_id.to_string()));
}

Manual Debug Implementation

If your source contains non-Debug types, implement Debug manually:

impl std::fmt::Debug for MySource {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("MySource")
            .field("name", &self.name())
            .finish()
    }
}

Testing

Create comprehensive tests:

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let source = MySource::new();

    // Test basic info
    println!("Source: {}", source.name());

    // Test browsing
    let result = source.browse("root").await?;
    println!("Items: {}", result.count());

    // Test URI resolution
    let uri = source.resolve_uri("track-1").await?;
    println!("URI: {}", uri);

    Ok(())
}

Further Reading