From 2149d1a79731178e01d04af4bd23c6e707f5f79d Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Thu, 16 Oct 2025 22:00:35 +0200 Subject: [PATCH] Elabore une crate pmosource --- Cargo.lock | 10 ++ Cargo.toml | 2 +- pmoparadise/Cargo.toml | 3 + pmoparadise/assets/default.webp | Bin 0 -> 9762 bytes pmoparadise/examples/show_source_image.rs | 46 +++++++++ pmoparadise/src/lib.rs | 2 + pmoparadise/src/source.rs | 70 ++++++++++++++ pmoqobuz/Cargo.toml | 3 + pmoqobuz/assets/default.webp | Bin 0 -> 3926 bytes pmoqobuz/examples/show_source_image.rs | 46 +++++++++ pmoqobuz/src/lib.rs | 2 + pmoqobuz/src/source.rs | 70 ++++++++++++++ pmosource/Cargo.toml | 17 ++++ pmosource/README.md | 113 ++++++++++++++++++++++ pmosource/examples/show_sources.rs | 66 +++++++++++++ pmosource/src/lib.rs | 112 +++++++++++++++++++++ 16 files changed, 561 insertions(+), 1 deletion(-) create mode 100644 pmoparadise/assets/default.webp create mode 100644 pmoparadise/examples/show_source_image.rs create mode 100644 pmoparadise/src/source.rs create mode 100644 pmoqobuz/assets/default.webp create mode 100644 pmoqobuz/examples/show_source_image.rs create mode 100644 pmoqobuz/src/source.rs create mode 100644 pmosource/Cargo.toml create mode 100644 pmosource/README.md create mode 100644 pmosource/examples/show_sources.rs create mode 100644 pmosource/src/lib.rs 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 0000000000000000000000000000000000000000..4a7000b6d885910b1da3282f2bcfd5e2b4bc5ffd GIT binary patch literal 9762 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..32bff5728986053b936fb52e5a3a05e23e627487 GIT binary patch literal 3926 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 k4|nH 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"); + } +}