diff --git a/Cargo.lock b/Cargo.lock index e8159dea..e0cebdc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2355,6 +2355,7 @@ dependencies = [ "hound", "pmodidl", "pmoserver", + "pmosource", "pmoupnp", "reqwest", "serde", @@ -2384,6 +2385,7 @@ dependencies = [ "pmocovers", "pmodidl", "pmoserver", + "pmosource", "reqwest", "serde", "serde_json", @@ -2418,6 +2420,14 @@ dependencies = [ "utoipa-swagger-ui", ] +[[package]] +name = "pmosource" +version = "0.1.0" +dependencies = [ + "image", + "thiserror 1.0.69", +] + [[package]] name = "pmoupnp" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 4abb736b..0de89299 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] 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"] diff --git a/pmoparadise/Cargo.toml b/pmoparadise/Cargo.toml index d2aba156..0249ae57 100644 --- a/pmoparadise/Cargo.toml +++ b/pmoparadise/Cargo.toml @@ -45,6 +45,9 @@ pmoserver = { path = "../pmoserver", optional = true } pmodidl = { path = "../pmodidl", optional = true } uuid = { version = "1.18", optional = true } +# Common music source traits +pmosource = { path = "../pmosource" } + [features] default = ["metadata-only"] # Mode métadonnées seules (pas de décodage FLAC) diff --git a/pmoparadise/assets/default.webp b/pmoparadise/assets/default.webp new file mode 100644 index 00000000..4a7000b6 Binary files /dev/null and b/pmoparadise/assets/default.webp differ diff --git a/pmoparadise/examples/show_source_image.rs b/pmoparadise/examples/show_source_image.rs new file mode 100644 index 00000000..d644de5f --- /dev/null +++ b/pmoparadise/examples/show_source_image.rs @@ -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> { + // 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(()) +} diff --git a/pmoparadise/src/lib.rs b/pmoparadise/src/lib.rs index 97ac53e0..6eeac657 100644 --- a/pmoparadise/src/lib.rs +++ b/pmoparadise/src/lib.rs @@ -195,6 +195,7 @@ pub mod client; pub mod error; pub mod models; +pub mod source; pub mod stream; #[cfg(feature = "per-track")] @@ -207,6 +208,7 @@ pub mod mediaserver; pub use client::{ClientBuilder, RadioParadiseClient}; pub use error::{Error, Result}; pub use models::{Bitrate, Block, DurationMs, EventId, NowPlaying, Song}; +pub use source::RadioParadiseSource; pub use stream::BlockStream; #[cfg(feature = "per-track")] diff --git a/pmoparadise/src/source.rs b/pmoparadise/src/source.rs new file mode 100644 index 00000000..7068400a --- /dev/null +++ b/pmoparadise/src/source.rs @@ -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"); + } +} diff --git a/pmoqobuz/Cargo.toml b/pmoqobuz/Cargo.toml index 36a1e1ec..eb22f0e7 100644 --- a/pmoqobuz/Cargo.toml +++ b/pmoqobuz/Cargo.toml @@ -47,6 +47,9 @@ axum = { version = "0.8", optional = true } # Documentation OpenAPI utoipa = { version = "5.3", optional = true } +# Common music source traits +pmosource = { path = "../pmosource" } + [features] default = [] # Feature pour activer les extensions pmoserver diff --git a/pmoqobuz/assets/default.webp b/pmoqobuz/assets/default.webp new file mode 100644 index 00000000..32bff572 Binary files /dev/null and b/pmoqobuz/assets/default.webp differ diff --git a/pmoqobuz/examples/show_source_image.rs b/pmoqobuz/examples/show_source_image.rs new file mode 100644 index 00000000..d7a01521 --- /dev/null +++ b/pmoqobuz/examples/show_source_image.rs @@ -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> { + // 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(()) +} diff --git a/pmoqobuz/src/lib.rs b/pmoqobuz/src/lib.rs index 2eb4649a..cc776f5c 100644 --- a/pmoqobuz/src/lib.rs +++ b/pmoqobuz/src/lib.rs @@ -151,6 +151,7 @@ pub mod client; pub mod didl; pub mod error; pub mod models; +pub mod source; // Extension pmoserver (feature-gated) #[cfg(feature = "pmoserver")] @@ -165,6 +166,7 @@ mod pmoserver_impl; pub use client::QobuzClient; pub use error::{QobuzError, Result}; pub use models::{Album, Artist, AudioFormat, Genre, Playlist, SearchResult, Track}; +pub use source::QobuzSource; /// Ré-exporte les types DIDL pour faciliter l'utilisation pub use didl::ToDIDL; diff --git a/pmoqobuz/src/source.rs b/pmoqobuz/src/source.rs new file mode 100644 index 00000000..7edbde34 --- /dev/null +++ b/pmoqobuz/src/source.rs @@ -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"); + } +} diff --git a/pmosource/Cargo.toml b/pmosource/Cargo.toml new file mode 100644 index 00000000..32f61009 --- /dev/null +++ b/pmosource/Cargo.toml @@ -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"] } diff --git a/pmosource/README.md b/pmosource/README.md new file mode 100644 index 00000000..9943bfc5 --- /dev/null +++ b/pmosource/README.md @@ -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**: `/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 diff --git a/pmosource/examples/show_sources.rs b/pmosource/examples/show_sources.rs new file mode 100644 index 00000000..02e27321 --- /dev/null +++ b/pmosource/examples/show_sources.rs @@ -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> = 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."); +} diff --git a/pmosource/src/lib.rs b/pmosource/src/lib.rs new file mode 100644 index 00000000..635134a8 --- /dev/null +++ b/pmosource/src/lib.rs @@ -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 = std::result::Result; + +/// 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"); + } +}