Crée la crate pmoplaylist

This commit is contained in:
2025-10-16 22:13:00 +02:00
parent 2149d1a797
commit b22a82bb50
25 changed files with 5482 additions and 189 deletions

460
pmosource/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,460 @@
# PMOSource Architecture
This document describes the architecture and design decisions for the `pmosource` crate.
## Overview
`pmosource` provides a unified abstraction layer for all music sources in the PMOMusic ecosystem. It defines the `MusicSource` trait that all concrete music sources (Radio Paradise, Qobuz, local playlists, etc.) must implement.
## Design Goals
1. **Unified Interface**: Single trait for all music source types
2. **UPnP/OpenHome Compatible**: Support ContentDirectory browsing and DIDL-Lite
3. **Cache Integration**: Seamless integration with `pmoaudiocache` and `pmocovers`
4. **Change Tracking**: Support for UPnP event notifications via `update_id` and `last_change`
5. **FIFO Support**: Dynamic sources (radios) can manage track queues
6. **Thread Safety**: All sources must be `Send + Sync` for async servers
7. **No Network Code**: Pure abstraction layer, no HTTP/network implementation
## Architecture Diagram
```
┌─────────────────────────────────────────────────────────────────┐
│ PMOMusic Server │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ MusicSource Registry │ │
│ │ - Manage multiple sources │ │
│ │ - Aggregate content for ContentDirectory │ │
│ │ - Handle browse/search requests │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────┼──────────────────┐ │
│ │ │ │ │
│ ┌────▼────┐ ┌─────▼────┐ ┌─────▼────┐ │
│ │ Radio │ │ Qobuz │ │ Local │ │
│ │Paradise │ │ Source │ │ Playlist │ │
│ └────┬────┘ └─────┬────┘ └─────┬────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ │ │
│ implements MusicSource trait │
└─────────────────────────────┬───────────────────────────────────┘
┌────────────────────┴────────────────────┐
│ │
┌────▼─────┐ ┌─────▼──────┐
│pmoplaylist│ │ pmodidl │
│ FIFO │ │ DIDL-Lite │
└──────────┘ └────────────┘
│ │
┌────▼─────────┐ ┌────▼────────┐
│pmoaudiocache │ │ pmocovers │
│ Audio files │ │ Images │
└──────────────┘ └─────────────┘
```
## Core Trait: `MusicSource`
The `MusicSource` trait is divided into 5 logical sections:
### 1. Basic Information
```rust
fn name(&self) -> &str;
fn id(&self) -> &str;
fn default_image(&self) -> &[u8];
fn default_image_mime_type(&self) -> &str;
```
These methods provide basic metadata about the source:
- **name**: Human-readable display name
- **id**: Unique identifier for routing and container IDs
- **default_image**: Embedded WebP logo (300x300px)
- **default_image_mime_type**: Always "image/webp"
### 2. ContentDirectory Navigation
```rust
async fn root_container(&self) -> Result<Container>;
async fn browse(&self, object_id: &str) -> Result<BrowseResult>;
async fn resolve_uri(&self, object_id: &str) -> Result<String>;
```
These methods support UPnP ContentDirectory Service:
- **root_container**: Returns the top-level container for this source
- **browse**: Returns children of a given container (sub-containers or items)
- **resolve_uri**: Resolves the actual streaming URI for a track (checks caches)
### 3. FIFO Management
```rust
fn supports_fifo(&self) -> bool;
async fn append_track(&self, track: Item) -> Result<()>;
async fn remove_oldest(&self) -> Result<Option<Item>>;
```
For dynamic sources (radios, streaming services):
- **supports_fifo**: Indicates if source uses a FIFO queue
- **append_track**: Adds track to queue (auto-removes oldest if capacity reached)
- **remove_oldest**: Manually removes oldest track
### 4. Change Tracking
```rust
async fn update_id(&self) -> u32;
async fn last_change(&self) -> Option<SystemTime>;
```
For UPnP event notifications:
- **update_id**: Counter incremented on each change (wraps around)
- **last_change**: Timestamp of last modification
### 5. Pagination & Search
```rust
async fn get_items(&self, offset: usize, count: usize) -> Result<Vec<Item>>;
async fn search(&self, query: &str) -> Result<BrowseResult>;
```
For efficient browsing and searching:
- **get_items**: Paginated access to items
- **search**: Optional search (default: not supported)
## Source Types
### Dynamic Sources (with FIFO)
Examples: Radio Paradise, streaming radios, live playlists
**Characteristics:**
- `supports_fifo() = true`
- Uses `pmoplaylist::FifoPlaylist` internally
- `update_id` changes when tracks are added/removed
- Limited capacity (e.g., last 50 tracks)
- Items have dynamic URIs that may change
**Implementation Pattern:**
```rust
struct RadioSource {
playlist: FifoPlaylist,
track_cache: RwLock<HashMap<String, (String, Option<String>)>>,
}
impl MusicSource for RadioSource {
fn supports_fifo(&self) -> bool {
true
}
async fn append_track(&self, track: Item) -> Result<()> {
// Convert Item to pmoplaylist::Track
// Add to playlist
self.playlist.append_track(pmo_track).await;
Ok(())
}
async fn update_id(&self) -> u32 {
self.playlist.update_id().await
}
}
```
### Static Sources (without FIFO)
Examples: Local albums, fixed playlists, Qobuz albums
**Characteristics:**
- `supports_fifo() = false`
- `append_track()` returns `FifoNotSupported` error
- `update_id` is constant (0)
- `last_change()` may be None
- Items have stable URIs
**Implementation Pattern:**
```rust
struct AlbumSource {
items: Vec<Item>,
}
impl MusicSource for AlbumSource {
fn supports_fifo(&self) -> bool {
false
}
async fn append_track(&self, _: Item) -> Result<()> {
Err(MusicSourceError::FifoNotSupported)
}
async fn update_id(&self) -> u32 {
0 // Never changes
}
}
```
## Integration with PMOMusic Ecosystem
### pmoplaylist Integration
`pmoplaylist` provides the `FifoPlaylist` struct for managing dynamic track lists:
```rust
use pmoplaylist::{FifoPlaylist, Track};
let playlist = FifoPlaylist::new(
"radio-id".to_string(),
"Radio Name".to_string(),
50, // capacity
DEFAULT_IMAGE,
);
// Add tracks
playlist.append_track(Track::new("id", "title", "uri")).await;
// Get tracks
let tracks = playlist.get_items(0, 10).await;
// Track changes
let update_id = playlist.update_id().await;
let last_change = playlist.last_change().await;
```
**Benefits:**
- Automatic capacity management (FIFO behavior)
- Built-in change tracking
- Thread-safe (Arc<RwLock<>>)
### pmodidl Integration
All sources use `pmodidl` for DIDL-Lite generation:
```rust
use pmodidl::{Container, Item, Resource};
// Containers for browsing
let container = Container {
id: "source-id".to_string(),
parent_id: "0".to_string(),
title: "My Source".to_string(),
class: "object.container.playlistContainer".to_string(),
child_count: Some("10".to_string()),
containers: vec![],
items: vec![],
};
// Items for tracks
let item = Item {
id: "track-1".to_string(),
parent_id: "source-id".to_string(),
title: "Track Title".to_string(),
artist: Some("Artist".to_string()),
class: "object.item.audioItem.musicTrack".to_string(),
resources: vec![Resource {
url: "http://server/audio/track-1".to_string(),
protocol_info: "http-get:*:audio/flac:*".to_string(),
duration: Some("0:03:45".to_string()),
..Default::default()
}],
..Default::default()
};
```
### pmoaudiocache Integration
Sources can use `pmoaudiocache` to cache audio files locally:
```rust
async fn resolve_uri(&self, object_id: &str) -> Result<String> {
// Check if track is cached
if let Some(cached_pk) = self.get_cached_pk(object_id).await {
// Return cached URI (local FLAC file)
Ok(format!("{}/audio/cache/{}", self.cache_base_url, cached_pk))
} else {
// Return original streaming URI
Ok(self.get_original_uri(object_id))
}
}
```
**Benefits:**
- Local caching of streamed audio
- Automatic FLAC conversion
- Metadata extraction and merging
- Reduced bandwidth usage
### pmocovers Integration
Sources can use `pmocovers` to cache album art:
```rust
// Store cover art PK in track metadata
let album_art_url = format!("{}/covers/images/{}", base_url, cover_pk);
let item = Item {
album_art: Some(album_art_url),
..Default::default()
};
```
**Benefits:**
- Local caching of album art
- Automatic WebP conversion
- Multiple size variants
- Optimized delivery
## Error Handling
All fallible operations return `pmosource::Result<T>`:
```rust
pub enum MusicSourceError {
ImageLoadError(String),
InvalidImageFormat(String),
SourceUnavailable(String),
ObjectNotFound(String),
BrowseError(String),
SearchNotSupported,
FifoNotSupported,
CacheError(String),
UriResolutionError(String),
}
```
**Guidelines:**
- Use `ObjectNotFound` for invalid object IDs
- Use `BrowseError` for general browsing failures
- Use `SearchNotSupported` for sources without search
- Use `FifoNotSupported` for static sources
- Use `CacheError` for cache-related issues
## Thread Safety
All `MusicSource` implementations must be `Send + Sync`:
```rust
pub trait MusicSource: Debug + Send + Sync {
// ...
}
```
**Reasoning:**
- Sources may be shared across multiple async tasks
- UPnP server handles concurrent requests
- `Arc<dyn MusicSource>` enables efficient sharing
**Implementation:**
- Use `Arc<RwLock<>>` for mutable state
- Use `tokio::sync::RwLock` for async operations
- Avoid `Rc`, `RefCell`, or other non-thread-safe types
## Testing Strategy
### Unit Tests
Test each method independently:
```rust
#[tokio::test]
async fn test_root_container() {
let source = MySource::new();
let root = source.root_container().await.unwrap();
assert_eq!(root.id, "my-source");
}
```
### Integration Tests
Test complete workflows:
```rust
#[tokio::test]
async fn test_browse_and_resolve() {
let source = MySource::new();
let result = source.browse("container-1").await.unwrap();
for item in result.items() {
let uri = source.resolve_uri(&item.id).await.unwrap();
assert!(uri.starts_with("http://"));
}
}
```
### Example Tests
Run examples as integration tests:
```bash
cargo run --example radio_paradise
```
## Future Enhancements
Potential additions to the trait:
1. **Authentication**:
```rust
async fn authenticate(&mut self, credentials: Credentials) -> Result<()>;
fn is_authenticated(&self) -> bool;
```
2. **Quality Levels**:
```rust
fn available_qualities(&self) -> Vec<Quality>;
async fn set_quality(&mut self, quality: Quality) -> Result<()>;
```
3. **Favorites/Bookmarks**:
```rust
async fn add_favorite(&self, object_id: &str) -> Result<()>;
async fn list_favorites(&self) -> Result<Vec<Item>>;
```
4. **Recommendations**:
```rust
async fn get_recommendations(&self) -> Result<Vec<Item>>;
```
## Design Decisions
### Why async-trait?
- Native async traits don't support trait objects yet
- `async-trait` provides a clean macro-based solution
- Minimal performance overhead with good compiler optimizations
### Why separate FIFO methods?
- Clear distinction between dynamic and static sources
- Static sources can return `FifoNotSupported` immediately
- Allows future optimizations for FIFO-specific operations
### Why BrowseResult enum?
- Different sources return different types of results
- Some return only containers, some only items, some mixed
- Enum provides type-safe representation of all cases
### Why separate resolve_uri?
- Caching is a cross-cutting concern
- Separating resolution from browsing allows flexible caching strategies
- URI resolution may be expensive (check cache, fallback to original)
## Performance Considerations
1. **Caching**: Always check local caches before streaming
2. **Pagination**: Use `get_items(offset, count)` for large collections
3. **Lazy Loading**: Don't load all metadata upfront
4. **Arc Sharing**: Use `Arc<dyn MusicSource>` to avoid cloning
5. **RwLock Usage**: Prefer read locks when possible
## Versioning
The crate follows Semantic Versioning:
- **MAJOR**: Breaking changes to `MusicSource` trait
- **MINOR**: New trait methods (with default implementations)
- **PATCH**: Bug fixes, documentation, internal changes
Current version: **0.2.0**