Elabore une crate pmosource

This commit is contained in:
2025-10-16 22:00:35 +02:00
parent 68b0c89130
commit e0fbb11475
16 changed files with 561 additions and 1 deletions

10
Cargo.lock generated
View File

@@ -2355,6 +2355,7 @@ dependencies = [
"hound", "hound",
"pmodidl", "pmodidl",
"pmoserver", "pmoserver",
"pmosource",
"pmoupnp", "pmoupnp",
"reqwest", "reqwest",
"serde", "serde",
@@ -2384,6 +2385,7 @@ dependencies = [
"pmocovers", "pmocovers",
"pmodidl", "pmodidl",
"pmoserver", "pmoserver",
"pmosource",
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
@@ -2418,6 +2420,14 @@ dependencies = [
"utoipa-swagger-ui", "utoipa-swagger-ui",
] ]
[[package]]
name = "pmosource"
version = "0.1.0"
dependencies = [
"image",
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "pmoupnp" name = "pmoupnp"
version = "0.1.0" version = "0.1.0"

View File

@@ -1,3 +1,3 @@
[workspace] [workspace]
resolver = "3" resolver = "3"
members = ["PMOMusic", "pmoupnp", "pmomediarenderer", "pmoconfig", "pmoutils", "pmodidl", "pmoserver", "pmoapp", "pmocache", "pmocovers", "pmoaudiocache", "pmoaudio", "pmoqobuz", "pmoparadise"] members = ["PMOMusic", "pmoupnp", "pmomediarenderer", "pmoconfig", "pmoutils", "pmodidl", "pmoserver", "pmoapp", "pmocache", "pmocovers", "pmoaudiocache", "pmoaudio", "pmoqobuz", "pmoparadise", "pmosource"]

View File

@@ -45,6 +45,9 @@ pmoserver = { path = "../pmoserver", optional = true }
pmodidl = { path = "../pmodidl", optional = true } pmodidl = { path = "../pmodidl", optional = true }
uuid = { version = "1.18", optional = true } uuid = { version = "1.18", optional = true }
# Common music source traits
pmosource = { path = "../pmosource" }
[features] [features]
default = ["metadata-only"] default = ["metadata-only"]
# Mode métadonnées seules (pas de décodage FLAC) # Mode métadonnées seules (pas de décodage FLAC)

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@@ -0,0 +1,46 @@
//! Example showing how to access and save the Radio Paradise source image
//!
//! This example demonstrates:
//! - Getting source information via the MusicSource trait
//! - Accessing the embedded WebP image
//! - Optionally saving it to a file
use pmoparadise::RadioParadiseSource;
use pmosource::MusicSource;
use std::fs;
use std::io::Write;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create the source
let source = RadioParadiseSource;
// Display source information
println!("Music Source Information");
println!("========================");
println!("Name: {}", source.name());
println!("ID: {}", source.id());
println!("Image MIME type: {}", source.default_image_mime_type());
// Get the embedded image
let image_data = source.default_image();
println!("Embedded image size: {} bytes", image_data.len());
// Verify WebP format
if image_data.len() >= 12 {
let is_webp = &image_data[0..4] == b"RIFF" && &image_data[8..12] == b"WEBP";
println!("Valid WebP format: {}", is_webp);
}
// Optional: save to file
if std::env::args().any(|arg| arg == "--save") {
let filename = format!("{}_default.webp", source.id());
let mut file = fs::File::create(&filename)?;
file.write_all(image_data)?;
println!("\nImage saved to: {}", filename);
println!("You can view it with: open {}", filename);
} else {
println!("\nTo save the image to disk, run with: --save");
}
Ok(())
}

View File

@@ -195,6 +195,7 @@
pub mod client; pub mod client;
pub mod error; pub mod error;
pub mod models; pub mod models;
pub mod source;
pub mod stream; pub mod stream;
#[cfg(feature = "per-track")] #[cfg(feature = "per-track")]
@@ -207,6 +208,7 @@ pub mod mediaserver;
pub use client::{ClientBuilder, RadioParadiseClient}; pub use client::{ClientBuilder, RadioParadiseClient};
pub use error::{Error, Result}; pub use error::{Error, Result};
pub use models::{Bitrate, Block, DurationMs, EventId, NowPlaying, Song}; pub use models::{Bitrate, Block, DurationMs, EventId, NowPlaying, Song};
pub use source::RadioParadiseSource;
pub use stream::BlockStream; pub use stream::BlockStream;
#[cfg(feature = "per-track")] #[cfg(feature = "per-track")]

70
pmoparadise/src/source.rs Normal file
View File

@@ -0,0 +1,70 @@
//! Music source implementation for Radio Paradise
//!
//! This module implements the [`pmosource::MusicSource`] trait for Radio Paradise,
//! providing access to the service's default image and identification information.
use pmosource::MusicSource;
/// Default image for Radio Paradise (300x300 WebP, embedded in binary)
const DEFAULT_IMAGE: &[u8] = include_bytes!("../assets/default.webp");
/// Radio Paradise music source
///
/// This struct implements the [`MusicSource`] trait to provide
/// standardized access to Radio Paradise's identification and branding.
///
/// # Examples
///
/// ```
/// use pmoparadise::RadioParadiseSource;
/// use pmosource::MusicSource;
///
/// let source = RadioParadiseSource;
/// assert_eq!(source.name(), "Radio Paradise");
/// assert_eq!(source.id(), "radio-paradise");
///
/// // Get default image as WebP bytes
/// let image_data = source.default_image();
/// assert!(image_data.len() > 0);
/// ```
#[derive(Debug, Clone, Copy, Default)]
pub struct RadioParadiseSource;
impl MusicSource for RadioParadiseSource {
fn name(&self) -> &str {
"Radio Paradise"
}
fn id(&self) -> &str {
"radio-paradise"
}
fn default_image(&self) -> &[u8] {
DEFAULT_IMAGE
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_source_info() {
let source = RadioParadiseSource;
assert_eq!(source.name(), "Radio Paradise");
assert_eq!(source.id(), "radio-paradise");
assert_eq!(source.default_image_mime_type(), "image/webp");
}
#[test]
fn test_default_image_present() {
let source = RadioParadiseSource;
let image = source.default_image();
assert!(image.len() > 0, "Default image should not be empty");
// Check WebP magic bytes (RIFF...WEBP)
assert!(image.len() >= 12, "Image too small to be valid WebP");
assert_eq!(&image[0..4], b"RIFF", "Missing RIFF header");
assert_eq!(&image[8..12], b"WEBP", "Missing WEBP signature");
}
}

View File

@@ -47,6 +47,9 @@ axum = { version = "0.8", optional = true }
# Documentation OpenAPI # Documentation OpenAPI
utoipa = { version = "5.3", optional = true } utoipa = { version = "5.3", optional = true }
# Common music source traits
pmosource = { path = "../pmosource" }
[features] [features]
default = [] default = []
# Feature pour activer les extensions pmoserver # Feature pour activer les extensions pmoserver

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,46 @@
//! Example showing how to access and save the Qobuz source image
//!
//! This example demonstrates:
//! - Getting source information via the MusicSource trait
//! - Accessing the embedded WebP image
//! - Optionally saving it to a file
use pmoqobuz::QobuzSource;
use pmosource::MusicSource;
use std::fs;
use std::io::Write;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create the source
let source = QobuzSource;
// Display source information
println!("Music Source Information");
println!("========================");
println!("Name: {}", source.name());
println!("ID: {}", source.id());
println!("Image MIME type: {}", source.default_image_mime_type());
// Get the embedded image
let image_data = source.default_image();
println!("Embedded image size: {} bytes", image_data.len());
// Verify WebP format
if image_data.len() >= 12 {
let is_webp = &image_data[0..4] == b"RIFF" && &image_data[8..12] == b"WEBP";
println!("Valid WebP format: {}", is_webp);
}
// Optional: save to file
if std::env::args().any(|arg| arg == "--save") {
let filename = format!("{}_default.webp", source.id());
let mut file = fs::File::create(&filename)?;
file.write_all(image_data)?;
println!("\nImage saved to: {}", filename);
println!("You can view it with: open {}", filename);
} else {
println!("\nTo save the image to disk, run with: --save");
}
Ok(())
}

View File

@@ -151,6 +151,7 @@ pub mod client;
pub mod didl; pub mod didl;
pub mod error; pub mod error;
pub mod models; pub mod models;
pub mod source;
// Extension pmoserver (feature-gated) // Extension pmoserver (feature-gated)
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
@@ -165,6 +166,7 @@ mod pmoserver_impl;
pub use client::QobuzClient; pub use client::QobuzClient;
pub use error::{QobuzError, Result}; pub use error::{QobuzError, Result};
pub use models::{Album, Artist, AudioFormat, Genre, Playlist, SearchResult, Track}; pub use models::{Album, Artist, AudioFormat, Genre, Playlist, SearchResult, Track};
pub use source::QobuzSource;
/// Ré-exporte les types DIDL pour faciliter l'utilisation /// Ré-exporte les types DIDL pour faciliter l'utilisation
pub use didl::ToDIDL; pub use didl::ToDIDL;

70
pmoqobuz/src/source.rs Normal file
View File

@@ -0,0 +1,70 @@
//! Music source implementation for Qobuz
//!
//! This module implements the [`pmosource::MusicSource`] trait for Qobuz,
//! providing access to the service's default image and identification information.
use pmosource::MusicSource;
/// Default image for Qobuz (300x300 WebP, embedded in binary)
const DEFAULT_IMAGE: &[u8] = include_bytes!("../assets/default.webp");
/// Qobuz music source
///
/// This struct implements the [`MusicSource`] trait to provide
/// standardized access to Qobuz's identification and branding.
///
/// # Examples
///
/// ```
/// use pmoqobuz::QobuzSource;
/// use pmosource::MusicSource;
///
/// let source = QobuzSource;
/// assert_eq!(source.name(), "Qobuz");
/// assert_eq!(source.id(), "qobuz");
///
/// // Get default image as WebP bytes
/// let image_data = source.default_image();
/// assert!(image_data.len() > 0);
/// ```
#[derive(Debug, Clone, Copy, Default)]
pub struct QobuzSource;
impl MusicSource for QobuzSource {
fn name(&self) -> &str {
"Qobuz"
}
fn id(&self) -> &str {
"qobuz"
}
fn default_image(&self) -> &[u8] {
DEFAULT_IMAGE
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_source_info() {
let source = QobuzSource;
assert_eq!(source.name(), "Qobuz");
assert_eq!(source.id(), "qobuz");
assert_eq!(source.default_image_mime_type(), "image/webp");
}
#[test]
fn test_default_image_present() {
let source = QobuzSource;
let image = source.default_image();
assert!(image.len() > 0, "Default image should not be empty");
// Check WebP magic bytes (RIFF...WEBP)
assert!(image.len() >= 12, "Image too small to be valid WebP");
assert_eq!(&image[0..4], b"RIFF", "Missing RIFF header");
assert_eq!(&image[8..12], b"WEBP", "Missing WEBP signature");
}
}

17
pmosource/Cargo.toml Normal file
View File

@@ -0,0 +1,17 @@
[package]
name = "pmosource"
version = "0.1.0"
edition = "2021"
authors = ["PMOMusic Contributors"]
description = "Common traits and types for PMOMusic sources"
license = "MIT OR Apache-2.0"
repository = "https://github.com/yourusername/pmomusic"
keywords = ["music", "source", "streaming"]
categories = ["multimedia"]
[dependencies]
# Gestion des erreurs
thiserror = "1.0"
# Image format support
image = { version = "0.25", default-features = false, features = ["webp"] }

113
pmosource/README.md Normal file
View File

@@ -0,0 +1,113 @@
# pmosource
Common traits and types for PMOMusic sources.
## Overview
`pmosource` provides the foundational abstractions for different music sources in the PMOMusic ecosystem, such as Radio Paradise, Qobuz, and potentially others in the future.
## Features
- **`MusicSource` trait**: Common interface for all music sources
- **Default images**: Standardized 300x300px WebP images embedded in binaries
- **Source identification**: Consistent naming and ID scheme
## Usage
### Implementing the trait
```rust
use pmosource::MusicSource;
const DEFAULT_IMAGE: &[u8] = include_bytes!("../assets/default.webp");
#[derive(Debug)]
pub struct MyMusicSource;
impl MusicSource for MyMusicSource {
fn name(&self) -> &str {
"My Music Service"
}
fn id(&self) -> &str {
"my-music-service"
}
fn default_image(&self) -> &[u8] {
DEFAULT_IMAGE
}
}
```
### Using a music source
```rust
use pmosource::MusicSource;
use pmoparadise::RadioParadiseSource;
use pmoqobuz::QobuzSource;
let rp = RadioParadiseSource;
let qobuz = QobuzSource;
println!("Source: {} ({})", rp.name(), rp.id());
println!("Image size: {} bytes", rp.default_image().len());
```
## Image Format
All default images should be:
- **Format**: WebP
- **Dimensions**: 300x300 pixels (square)
- **Quality**: 85 (good balance between size and quality)
- **Location**: `<crate>/assets/default.webp`
### Converting images
Use the provided Python script or similar tool:
```python
from PIL import Image
def convert_to_webp(input_path, output_path, size=300):
img = Image.open(input_path)
# Convert to RGB if necessary
if img.mode not in ('RGB', 'RGBA'):
img = img.convert('RGB')
# Make it square (center crop)
width, height = img.size
if width != height:
min_dim = min(width, height)
left = (width - min_dim) // 2
top = (height - min_dim) // 2
right = left + min_dim
bottom = top + min_dim
img = img.crop((left, top, right, bottom))
# Resize to target size
img = img.resize((size, size), Image.Resampling.LANCZOS)
# Save as WebP
img.save(output_path, 'WEBP', quality=85, method=6)
```
## Current Implementations
- **pmoparadise**: Radio Paradise
- **pmoqobuz**: Qobuz
## Future Enhancements
The `MusicSource` trait can be extended with additional methods such as:
- Authentication status
- Available quality levels
- Streaming capabilities
- Search functionality
- Playlist management
- And more...
## License
MIT OR Apache-2.0

View File

@@ -0,0 +1,66 @@
//! 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.");
}

112
pmosource/src/lib.rs Normal file
View File

@@ -0,0 +1,112 @@
//! # PMOSource
//!
//! 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, etc.
use std::fmt::Debug;
/// Standard size for default images (300x300 pixels)
pub const DEFAULT_IMAGE_SIZE: u32 = 300;
/// Error types for music source operations
#[derive(Debug, thiserror::Error)]
pub enum MusicSourceError {
#[error("Failed to load default image: {0}")]
ImageLoadError(String),
#[error("Invalid image format: {0}")]
InvalidImageFormat(String),
#[error("Source not available: {0}")]
SourceUnavailable(String),
}
/// Result type for music source operations
pub type Result<T> = std::result::Result<T, MusicSourceError>;
/// Main trait for music sources
///
/// This trait defines the common interface that all music sources must implement.
/// It provides methods for:
/// - Getting the source name and identification
/// - Retrieving default images/logos
/// - Other common operations (to be extended)
pub trait MusicSource: Debug + Send + Sync {
/// Returns the human-readable name of the music source
///
/// # Examples
///
/// ```ignore
/// assert_eq!(source.name(), "Radio Paradise");
/// ```
fn name(&self) -> &str;
/// Returns a unique identifier for the music source
///
/// This is typically a lowercase, hyphenated version of the name
/// suitable for use in URLs, file names, etc.
///
/// # Examples
///
/// ```ignore
/// assert_eq!(source.id(), "radio-paradise");
/// ```
fn id(&self) -> &str;
/// Returns the default image/logo for this source as WebP bytes
///
/// The image should be square (300x300 pixels) and in WebP format.
/// This is embedded in the binary for offline availability.
///
/// # Returns
///
/// A byte slice containing the WebP-encoded image data
///
/// # Examples
///
/// ```ignore
/// let image_data = source.default_image();
/// assert!(image_data.len() > 0);
/// ```
fn default_image(&self) -> &[u8];
/// Returns the MIME type of the default image
///
/// By default, this returns "image/webp" since all default images
/// should be in WebP format.
fn default_image_mime_type(&self) -> &str {
"image/webp"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug)]
struct TestSource;
impl MusicSource for TestSource {
fn name(&self) -> &str {
"Test Source"
}
fn id(&self) -> &str {
"test-source"
}
fn default_image(&self) -> &[u8] {
&[]
}
}
#[test]
fn test_music_source_trait() {
let source = TestSource;
assert_eq!(source.name(), "Test Source");
assert_eq!(source.id(), "test-source");
assert_eq!(source.default_image_mime_type(), "image/webp");
}
}