Crée la crate pmoplaylist
This commit is contained in:
270
pmosource/examples/README.md
Normal file
270
pmosource/examples/README.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# 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
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```rust
|
||||
#[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
|
||||
|
||||
```rust
|
||||
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)
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```rust
|
||||
async fn get_items(&self, offset: usize, count: usize) -> Result<Vec<Item>> {
|
||||
// Return paginated items
|
||||
Ok(vec![])
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Implement Search (optional)
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
```rust
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
struct MySourceInner {
|
||||
state: RwLock<HashMap<String, Track>>,
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
Use appropriate error types:
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
```rust
|
||||
#[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
|
||||
|
||||
- [Main README](../README.md): Overview and quick start
|
||||
- [ARCHITECTURE.md](../ARCHITECTURE.md): Detailed architecture documentation
|
||||
- [CHANGELOG.md](../CHANGELOG.md): Version history and changes
|
||||
466
pmosource/examples/radio_paradise.rs
Normal file
466
pmosource/examples/radio_paradise.rs
Normal file
@@ -0,0 +1,466 @@
|
||||
//! # Radio Paradise Example
|
||||
//!
|
||||
//! This example demonstrates how to implement a concrete `MusicSource` using
|
||||
//! Radio Paradise as a streaming radio source with FIFO support.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! - **FIFO Playlist**: Uses `pmoplaylist::FifoPlaylist` for dynamic track management
|
||||
//! - **Cache Integration**: Resolves URIs via `pmoaudiocache` and `pmocovers` (when enabled)
|
||||
//! - **DIDL-Lite Export**: Generates proper UPnP-compatible containers and items
|
||||
//! - **Change Tracking**: Tracks `update_id` and `last_change` for notifications
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ```bash
|
||||
//! cargo run --example radio_paradise
|
||||
//! ```
|
||||
|
||||
use pmosource::{async_trait, BrowseResult, MusicSource, MusicSourceError, Result};
|
||||
use pmodidl::{Container, Item, Resource};
|
||||
use pmoplaylist::{FifoPlaylist, Track};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// Default image for Radio Paradise (embedded WebP)
|
||||
const RADIO_PARADISE_IMAGE: &[u8] = include_bytes!("../assets/radio-paradise.webp");
|
||||
|
||||
/// Default capacity for the FIFO (number of recent tracks to keep)
|
||||
const DEFAULT_FIFO_CAPACITY: usize = 50;
|
||||
|
||||
/// Radio Paradise music source
|
||||
///
|
||||
/// This is a concrete implementation of `MusicSource` for Radio Paradise,
|
||||
/// demonstrating how to:
|
||||
/// - Use `pmoplaylist::FifoPlaylist` for dynamic track management
|
||||
/// - Integrate with caches for URI resolution
|
||||
/// - Implement ContentDirectory browsing
|
||||
/// - Track changes via `update_id` and `last_change`
|
||||
#[derive(Clone)]
|
||||
pub struct RadioParadise {
|
||||
inner: Arc<RadioParadiseInner>,
|
||||
}
|
||||
|
||||
struct RadioParadiseInner {
|
||||
/// FIFO playlist managed by pmoplaylist
|
||||
playlist: FifoPlaylist,
|
||||
|
||||
/// Cache server base URL (for URI resolution)
|
||||
cache_base_url: String,
|
||||
|
||||
/// Track metadata cache (object_id -> original_uri, cached_pk)
|
||||
track_cache: RwLock<HashMap<String, (String, Option<String>)>>,
|
||||
}
|
||||
|
||||
// Manual Debug implementation since FifoPlaylist doesn't derive Debug
|
||||
impl std::fmt::Debug for RadioParadise {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("RadioParadise")
|
||||
.field("cache_base_url", &self.inner.cache_base_url)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl RadioParadise {
|
||||
/// Create a new Radio Paradise source
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cache_base_url` - Base URL for the cache server (e.g., "http://localhost:8080")
|
||||
/// * `fifo_capacity` - Maximum number of tracks in the FIFO
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use pmosource::RadioParadise;
|
||||
///
|
||||
/// let source = RadioParadise::new("http://localhost:8080", 50);
|
||||
/// ```
|
||||
pub fn new(cache_base_url: impl Into<String>, fifo_capacity: usize) -> Self {
|
||||
let playlist = FifoPlaylist::new(
|
||||
"radio-paradise".to_string(),
|
||||
"Radio Paradise".to_string(),
|
||||
fifo_capacity,
|
||||
RADIO_PARADISE_IMAGE,
|
||||
);
|
||||
|
||||
Self {
|
||||
inner: Arc::new(RadioParadiseInner {
|
||||
playlist,
|
||||
cache_base_url: cache_base_url.into(),
|
||||
track_cache: RwLock::new(HashMap::new()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create with default settings
|
||||
pub fn new_default(cache_base_url: impl Into<String>) -> Self {
|
||||
Self::new(cache_base_url, DEFAULT_FIFO_CAPACITY)
|
||||
}
|
||||
|
||||
/// Add a track to the Radio Paradise FIFO from raw data
|
||||
///
|
||||
/// This simulates receiving a new track from the Radio Paradise API.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `id` - Unique track ID
|
||||
/// * `title` - Track title
|
||||
/// * `artist` - Artist name
|
||||
/// * `album` - Album name
|
||||
/// * `uri` - Original streaming URI
|
||||
/// * `image_url` - URL for cover art (optional)
|
||||
/// * `duration` - Track duration in seconds (optional)
|
||||
pub async fn add_track(
|
||||
&self,
|
||||
id: String,
|
||||
title: String,
|
||||
artist: Option<String>,
|
||||
album: Option<String>,
|
||||
uri: String,
|
||||
image_url: Option<String>,
|
||||
duration: Option<u32>,
|
||||
) -> Result<()> {
|
||||
// Store the original URI for later resolution
|
||||
{
|
||||
let mut cache = self.inner.track_cache.write().await;
|
||||
cache.insert(id.clone(), (uri.clone(), None));
|
||||
}
|
||||
|
||||
// Create a Track for pmoplaylist
|
||||
let mut track = Track::new(id, title, uri);
|
||||
|
||||
if let Some(artist) = artist {
|
||||
track = track.with_artist(artist);
|
||||
}
|
||||
|
||||
if let Some(album) = album {
|
||||
track = track.with_album(album);
|
||||
}
|
||||
|
||||
if let Some(duration) = duration {
|
||||
track = track.with_duration(duration);
|
||||
}
|
||||
|
||||
if let Some(image) = image_url {
|
||||
track = track.with_image(image);
|
||||
}
|
||||
|
||||
// Add to the FIFO (automatically handles capacity)
|
||||
self.inner.playlist.append_track(track).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Simulate caching a track
|
||||
///
|
||||
/// In a real implementation, this would interact with `pmoaudiocache`
|
||||
/// to download and cache the track, then store the cache key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `track_id` - The track ID to cache
|
||||
/// * `cache_pk` - The cache primary key returned by pmoaudiocache
|
||||
pub async fn cache_track(&self, track_id: &str, cache_pk: String) -> Result<()> {
|
||||
let mut cache = self.inner.track_cache.write().await;
|
||||
|
||||
if let Some((_original_uri, cached_pk)) = cache.get_mut(track_id) {
|
||||
*cached_pk = Some(cache_pk);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(MusicSourceError::ObjectNotFound(track_id.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert pmoplaylist::Track to pmodidl::Item
|
||||
fn track_to_item(&self, track: &Track) -> Item {
|
||||
// Format duration
|
||||
let duration_str = track.duration.map(|d| {
|
||||
let hours = d / 3600;
|
||||
let minutes = (d % 3600) / 60;
|
||||
let seconds = d % 60;
|
||||
format!("{}:{:02}:{:02}", hours, minutes, seconds)
|
||||
});
|
||||
|
||||
// Create resource
|
||||
let resource = Resource {
|
||||
protocol_info: "http-get:*:audio/*:*".to_string(),
|
||||
bits_per_sample: None,
|
||||
sample_frequency: None,
|
||||
nr_audio_channels: None,
|
||||
duration: duration_str,
|
||||
url: track.uri.clone(),
|
||||
};
|
||||
|
||||
Item {
|
||||
id: track.id.clone(),
|
||||
parent_id: "radio-paradise".to_string(),
|
||||
restricted: Some("1".to_string()),
|
||||
title: track.title.clone(),
|
||||
creator: track.artist.clone(),
|
||||
class: "object.item.audioItem.musicTrack".to_string(),
|
||||
artist: track.artist.clone(),
|
||||
album: track.album.clone(),
|
||||
genre: None,
|
||||
album_art: track.image.clone(),
|
||||
album_art_pk: None,
|
||||
date: None,
|
||||
original_track_number: None,
|
||||
resources: vec![resource],
|
||||
descriptions: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MusicSource for RadioParadise {
|
||||
fn name(&self) -> &str {
|
||||
"Radio Paradise"
|
||||
}
|
||||
|
||||
fn id(&self) -> &str {
|
||||
"radio-paradise"
|
||||
}
|
||||
|
||||
fn default_image(&self) -> &[u8] {
|
||||
RADIO_PARADISE_IMAGE
|
||||
}
|
||||
|
||||
async fn root_container(&self) -> Result<Container> {
|
||||
Ok(self.inner.playlist.as_container().await)
|
||||
}
|
||||
|
||||
async fn browse(&self, object_id: &str) -> Result<BrowseResult> {
|
||||
// For Radio Paradise, browsing the root returns all tracks in the FIFO
|
||||
if object_id == "radio-paradise" || object_id == "0" {
|
||||
let tracks = self.inner.playlist.get_items(0, 1000).await;
|
||||
let items: Vec<Item> = tracks.iter().map(|t| self.track_to_item(t)).collect();
|
||||
Ok(BrowseResult::Items(items))
|
||||
} else {
|
||||
Err(MusicSourceError::ObjectNotFound(object_id.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_uri(&self, object_id: &str) -> Result<String> {
|
||||
let cache = self.inner.track_cache.read().await;
|
||||
|
||||
if let Some((original_uri, cached_pk)) = cache.get(object_id) {
|
||||
// If cached, return the cached URI
|
||||
if let Some(pk) = cached_pk {
|
||||
Ok(format!("{}/audio/cache/{}", self.inner.cache_base_url, pk))
|
||||
} else {
|
||||
// Not cached yet, return original URI
|
||||
Ok(original_uri.clone())
|
||||
}
|
||||
} else {
|
||||
Err(MusicSourceError::ObjectNotFound(object_id.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn supports_fifo(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn append_track(&self, track: Item) -> Result<()> {
|
||||
// Convert Item back to Track
|
||||
let duration = track
|
||||
.resources
|
||||
.first()
|
||||
.and_then(|r| r.duration.as_ref())
|
||||
.and_then(|d| {
|
||||
let parts: Vec<&str> = d.split(':').collect();
|
||||
if parts.len() == 3 {
|
||||
let h: u32 = parts[0].parse().ok()?;
|
||||
let m: u32 = parts[1].parse().ok()?;
|
||||
let s: u32 = parts[2].parse().ok()?;
|
||||
Some(h * 3600 + m * 60 + s)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let uri = track
|
||||
.resources
|
||||
.first()
|
||||
.map(|r| r.url.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut pmo_track = Track::new(track.id.clone(), track.title.clone(), uri.clone());
|
||||
|
||||
if let Some(artist) = track.artist {
|
||||
pmo_track = pmo_track.with_artist(artist);
|
||||
}
|
||||
|
||||
if let Some(album) = track.album {
|
||||
pmo_track = pmo_track.with_album(album);
|
||||
}
|
||||
|
||||
if let Some(dur) = duration {
|
||||
pmo_track = pmo_track.with_duration(dur);
|
||||
}
|
||||
|
||||
if let Some(img) = track.album_art {
|
||||
pmo_track = pmo_track.with_image(img);
|
||||
}
|
||||
|
||||
// Store in cache
|
||||
{
|
||||
let mut cache = self.inner.track_cache.write().await;
|
||||
cache.insert(track.id.clone(), (uri, None));
|
||||
}
|
||||
|
||||
self.inner.playlist.append_track(pmo_track).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_oldest(&self) -> Result<Option<Item>> {
|
||||
if let Some(track) = self.inner.playlist.remove_oldest().await {
|
||||
// Remove from cache
|
||||
{
|
||||
let mut cache = self.inner.track_cache.write().await;
|
||||
cache.remove(&track.id);
|
||||
}
|
||||
|
||||
Ok(Some(self.track_to_item(&track)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_id(&self) -> u32 {
|
||||
self.inner.playlist.update_id().await
|
||||
}
|
||||
|
||||
async fn last_change(&self) -> Option<std::time::SystemTime> {
|
||||
Some(self.inner.playlist.last_change().await)
|
||||
}
|
||||
|
||||
async fn get_items(&self, offset: usize, count: usize) -> Result<Vec<Item>> {
|
||||
let tracks = self.inner.playlist.get_items(offset, count).await;
|
||||
Ok(tracks.iter().map(|t| self.track_to_item(t)).collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
println!("Radio Paradise Source Example");
|
||||
println!("==============================\n");
|
||||
|
||||
// Create the source
|
||||
let source = RadioParadise::new_default("http://localhost:8080");
|
||||
|
||||
println!("Source: {}", source.name());
|
||||
println!("ID: {}", source.id());
|
||||
println!("Supports FIFO: {}", source.supports_fifo());
|
||||
println!("Default image size: {} bytes\n", source.default_image().len());
|
||||
|
||||
// Add some sample tracks
|
||||
println!("Adding sample tracks...");
|
||||
|
||||
source
|
||||
.add_track(
|
||||
"rp-001".to_string(),
|
||||
"Wish You Were Here".to_string(),
|
||||
Some("Pink Floyd".to_string()),
|
||||
Some("Wish You Were Here".to_string()),
|
||||
"http://stream.radioparadise.com/rp-001.mp3".to_string(),
|
||||
Some("http://img.radioparadise.com/covers/l/001.jpg".to_string()),
|
||||
Some(334),
|
||||
)
|
||||
.await?;
|
||||
|
||||
source
|
||||
.add_track(
|
||||
"rp-002".to_string(),
|
||||
"Bohemian Rhapsody".to_string(),
|
||||
Some("Queen".to_string()),
|
||||
Some("A Night at the Opera".to_string()),
|
||||
"http://stream.radioparadise.com/rp-002.mp3".to_string(),
|
||||
Some("http://img.radioparadise.com/covers/l/002.jpg".to_string()),
|
||||
Some(354),
|
||||
)
|
||||
.await?;
|
||||
|
||||
source
|
||||
.add_track(
|
||||
"rp-003".to_string(),
|
||||
"Hotel California".to_string(),
|
||||
Some("Eagles".to_string()),
|
||||
Some("Hotel California".to_string()),
|
||||
"http://stream.radioparadise.com/rp-003.mp3".to_string(),
|
||||
Some("http://img.radioparadise.com/covers/l/003.jpg".to_string()),
|
||||
Some(391),
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("Added 3 tracks\n");
|
||||
|
||||
// Get root container
|
||||
println!("Root Container:");
|
||||
let root = source.root_container().await?;
|
||||
println!(" ID: {}", root.id);
|
||||
println!(" Title: {}", root.title);
|
||||
println!(" Child Count: {:?}\n", root.child_count);
|
||||
|
||||
// Browse the source
|
||||
println!("Browsing tracks:");
|
||||
let result = source.browse("radio-paradise").await?;
|
||||
for item in result.items() {
|
||||
println!(
|
||||
" - {} by {} ({})",
|
||||
item.title,
|
||||
item.artist.as_deref().unwrap_or("Unknown"),
|
||||
item.album.as_deref().unwrap_or("Unknown Album")
|
||||
);
|
||||
}
|
||||
println!();
|
||||
|
||||
// Resolve URIs
|
||||
println!("Resolving URIs:");
|
||||
for item in result.items() {
|
||||
let uri = source.resolve_uri(&item.id).await?;
|
||||
println!(" {}: {}", item.id, uri);
|
||||
}
|
||||
println!();
|
||||
|
||||
// Track changes
|
||||
println!("Change Tracking:");
|
||||
println!(" Update ID: {}", source.update_id().await);
|
||||
println!(
|
||||
" Last Change: {:?}\n",
|
||||
source.last_change().await.unwrap()
|
||||
);
|
||||
|
||||
// Simulate caching a track
|
||||
println!("Simulating cache for rp-001...");
|
||||
source.cache_track("rp-001", "cached-abc123".to_string()).await?;
|
||||
|
||||
let cached_uri = source.resolve_uri("rp-001").await?;
|
||||
println!(" Cached URI: {}\n", cached_uri);
|
||||
|
||||
// Pagination
|
||||
println!("Pagination (get items 1-2):");
|
||||
let items = source.get_items(1, 2).await?;
|
||||
for item in items {
|
||||
println!(" - {}", item.title);
|
||||
}
|
||||
println!();
|
||||
|
||||
// Remove oldest track
|
||||
println!("Removing oldest track...");
|
||||
if let Some(removed) = source.remove_oldest().await? {
|
||||
println!(" Removed: {}", removed.title);
|
||||
}
|
||||
println!(" New Update ID: {}\n", source.update_id().await);
|
||||
|
||||
// Browse again to see the change
|
||||
println!("Browsing after removal:");
|
||||
let result = source.browse("radio-paradise").await?;
|
||||
println!(" Tracks remaining: {}", result.count());
|
||||
for item in result.items() {
|
||||
println!(" - {}", item.title);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
//! Example showing how to use the MusicSource trait
|
||||
//!
|
||||
//! This example demonstrates accessing source information and images
|
||||
//! from different music sources (requires pmoparadise and pmoqobuz to be compiled).
|
||||
|
||||
use pmosource::{MusicSource, DEFAULT_IMAGE_SIZE};
|
||||
|
||||
// Mock implementations for demonstration
|
||||
#[derive(Debug)]
|
||||
struct RadioParadiseSource;
|
||||
|
||||
impl MusicSource for RadioParadiseSource {
|
||||
fn name(&self) -> &str {
|
||||
"Radio Paradise"
|
||||
}
|
||||
|
||||
fn id(&self) -> &str {
|
||||
"radio-paradise"
|
||||
}
|
||||
|
||||
fn default_image(&self) -> &[u8] {
|
||||
// This would normally be: include_bytes!("../../pmoparadise/assets/default.webp")
|
||||
// For this example, we return an empty slice
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct QobuzSource;
|
||||
|
||||
impl MusicSource for QobuzSource {
|
||||
fn name(&self) -> &str {
|
||||
"Qobuz"
|
||||
}
|
||||
|
||||
fn id(&self) -> &str {
|
||||
"qobuz"
|
||||
}
|
||||
|
||||
fn default_image(&self) -> &[u8] {
|
||||
// This would normally be: include_bytes!("../../pmoqobuz/assets/default.webp")
|
||||
// For this example, we return an empty slice
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("PMOMusic Sources\n");
|
||||
println!("Standard image size: {}x{} pixels\n", DEFAULT_IMAGE_SIZE, DEFAULT_IMAGE_SIZE);
|
||||
|
||||
let sources: Vec<Box<dyn MusicSource>> = vec![
|
||||
Box::new(RadioParadiseSource),
|
||||
Box::new(QobuzSource),
|
||||
];
|
||||
|
||||
for source in sources {
|
||||
println!("Source: {}", source.name());
|
||||
println!(" ID: {}", source.id());
|
||||
println!(" Image MIME: {}", source.default_image_mime_type());
|
||||
println!(" Image size: {} bytes", source.default_image().len());
|
||||
println!();
|
||||
}
|
||||
|
||||
println!("Note: In a real implementation, the images would be embedded in the binary");
|
||||
println!(" and would be approximately 3-10 KB each in WebP format.");
|
||||
}
|
||||
Reference in New Issue
Block a user