//! Structures de données pour représenter les objets Qobuz use chrono::{DateTime, Utc}; use serde::{Deserialize, Deserializer, Serialize}; /// Désérialiseur flexible pour les IDs qui peuvent être des strings ou des integers pub(crate) fn deserialize_id<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { use serde::de::Error; use serde_json::Value; let value = Value::deserialize(deserializer)?; match value { Value::String(s) => Ok(s), Value::Number(n) => Ok(n.to_string()), _ => Err(Error::custom("ID must be a string or number")), } } /// Représente un artiste Qobuz #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Artist { /// Identifiant unique de l'artiste #[serde(deserialize_with = "deserialize_id")] pub id: String, /// Nom de l'artiste pub name: String, /// URL de l'image de l'artiste (optionnelle) #[serde(default)] pub image: Option, /// URL de l'image cachée localement (via pmocovers) #[serde(skip)] pub image_cached: Option, } /// Représente un album Qobuz #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Album { /// Identifiant unique de l'album #[serde(deserialize_with = "deserialize_id")] pub id: String, /// Titre de l'album pub title: String, /// Artiste principal de l'album pub artist: Artist, /// Nombre de pistes #[serde(default)] pub tracks_count: Option, /// Durée totale en secondes #[serde(default)] pub duration: Option, /// Date de sortie (format ISO 8601) #[serde(default)] pub release_date: Option, /// Timestamp Unix de sortie (released_at) #[serde(default)] pub released_at: Option, /// URL de l'image de couverture #[serde(default)] pub image: Option, /// URL de l'image cachée localement (via pmocovers) #[serde(skip)] pub image_cached: Option, /// Indique si l'album est disponible pour le streaming #[serde(default = "default_true")] pub streamable: bool, /// Description de l'album #[serde(default)] pub description: Option, /// Taux d'échantillonnage maximum (Hz) #[serde(default)] pub maximum_sampling_rate: Option, /// Profondeur de bits maximale #[serde(default)] pub maximum_bit_depth: Option, /// Genre(s) de l'album #[serde(default)] pub genres: Vec, /// Label de l'album #[serde(default)] pub label: Option, } /// Représente une piste (track) Qobuz #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Track { /// Identifiant unique de la piste #[serde(deserialize_with = "deserialize_id")] pub id: String, /// Titre de la piste pub title: String, /// Artiste de la piste (peut différer de l'artiste de l'album) pub performer: Option, /// Album contenant la piste pub album: Option, /// Durée en secondes pub duration: u32, /// Numéro de piste pub track_number: u32, /// Numéro de disque (pour les albums multi-disques) pub media_number: u32, /// Indique si la piste est disponible pour le streaming #[serde(default = "default_true")] pub streamable: bool, /// Type MIME du fichier audio (déterminé après obtention de l'URL) #[serde(skip)] pub mime_type: Option, /// Fréquence d'échantillonnage (Hz) #[serde(skip)] pub sample_rate: Option, /// Profondeur de bits #[serde(skip)] pub bit_depth: Option, /// Nombre de canaux audio #[serde(skip)] pub channels: Option, } /// Représente une playlist Qobuz #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Playlist { /// Identifiant unique de la playlist #[serde(deserialize_with = "deserialize_id")] pub id: String, /// Nom de la playlist pub name: String, /// Description de la playlist #[serde(default)] pub description: Option, /// Nombre de pistes #[serde(default)] pub tracks_count: Option, /// Durée totale en secondes #[serde(default)] pub duration: Option, /// URL de l'image de la playlist #[serde(default)] pub image: Option, /// URL de l'image cachée localement #[serde(skip)] pub image_cached: Option, /// Indique si c'est une playlist publique #[serde(default)] pub is_public: bool, /// Timestamp de dernière modification (Unix epoch secondes) #[serde(default)] pub updated_at: Option, /// Propriétaire de la playlist #[serde(default)] pub owner: Option, } /// Propriétaire d'une playlist #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PlaylistOwner { /// Identifiant de l'utilisateur pub id: u64, /// Nom de l'utilisateur pub name: String, } /// Représente un genre musical #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Genre { /// Identifiant du genre (peut être None pour "All Genres") pub id: Option, /// Nom du genre pub name: String, /// Genres enfants #[serde(default)] pub children: Vec, } /// Résultats de recherche #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct SearchResult { /// Albums trouvés #[serde(default)] pub albums: Vec, /// Artistes trouvés #[serde(default)] pub artists: Vec, /// Pistes trouvées #[serde(default)] pub tracks: Vec, /// Playlists trouvées #[serde(default)] pub playlists: Vec, } /// Informations sur un fichier de streaming #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StreamInfo { /// URL de streaming pub url: String, /// Type MIME pub mime_type: String, /// Fréquence d'échantillonnage (kHz) pub sampling_rate: f64, /// Profondeur de bits pub bit_depth: u32, /// Format ID Qobuz pub format_id: u8, /// Date d'expiration de l'URL #[serde(skip)] pub expires_at: DateTime, } /// Informations pour le streaming CMAF d'un track. /// /// Retourné par `QobuzClient::get_cmaf_stream_info`. /// L'appelant peut ensuite appeler `pmoqobuz::cmaf::download_full` pour /// obtenir les bytes FLAC déchiffrés, ou streamer les segments manuellement. #[derive(Debug)] pub struct CmafStreamInfo { /// Modèle d'URL des segments, avec le placeholder `$SEGMENT$`. pub url_template: String, /// Nombre de segments audio (hors segment init s=0). pub n_segments: u8, /// Clé AES-128 déchiffrée pour décoder les frames FLAC. pub content_key: [u8; 16], /// Header FLAC extrait du segment init (à placer en tête du flux décodé). pub flac_header: Vec, /// Table des segments avec taille et compte d'échantillons. pub segment_table: Vec, /// Format ID Qobuz. pub format_id: u32, /// Fréquence d'échantillonnage (Hz). pub sampling_rate: Option, /// Profondeur de bits. pub bit_depth: Option, } /// Format audio demandé pour le streaming #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[repr(u8)] #[allow(non_camel_case_types)] pub enum AudioFormat { /// MP3 320 kbps Mp3_320 = 5, /// FLAC 16 bit / 44.1 kHz (CD Quality) Flac_Lossless = 6, /// FLAC 24 bit / jusqu'à 96 kHz (Hi-Res) Flac_HiRes_96 = 7, /// FLAC 24 bit / jusqu'à 192 kHz (Hi-Res+) Flac_HiRes_192 = 27, } impl AudioFormat { /// Retourne l'ID du format pour l'API Qobuz pub fn id(&self) -> u8 { *self as u8 } /// Retourne une description lisible du format pub fn description(&self) -> &'static str { match self { AudioFormat::Mp3_320 => "MP3 320 kbps", AudioFormat::Flac_Lossless => "FLAC 16 bit / 44.1 kHz", AudioFormat::Flac_HiRes_96 => "FLAC 24 bit / up to 96 kHz", AudioFormat::Flac_HiRes_192 => "FLAC 24 bit / up to 192 kHz", } } /// Retourne le type MIME associé pub fn mime_type(&self) -> &'static str { match self { AudioFormat::Mp3_320 => "audio/mpeg", _ => "audio/flac", } } } impl Default for AudioFormat { fn default() -> Self { AudioFormat::Flac_Lossless } } // Helper functions fn default_true() -> bool { true } impl Artist { /// Crée un nouvel artiste avec un ID et un nom pub fn new(id: impl Into, name: impl Into) -> Self { Self { id: id.into(), name: name.into(), image: None, image_cached: None, } } } impl Album { /// Retourne un titre formaté avec les informations audio si disponibles pub fn formatted_title(&self) -> String { if let (Some(rate), Some(depth)) = (self.maximum_sampling_rate, self.maximum_bit_depth) { // Convertir Hz en kHz, en gérant les valeurs qui pourraient déjà être en kHz let rate_khz = if rate > 1000.0 { rate / 1000.0 } else { rate }; format!("{} ({:.1} kHz / {} bits)", self.title, rate_khz, depth) } else { self.title.clone() } } /// Vérifie si l'album est disponible pour le streaming pub fn is_available(&self) -> bool { self.streamable } } impl Track { /// Retourne l'artiste à afficher (performer ou artiste de l'album) pub fn display_artist(&self) -> Option<&Artist> { self.performer .as_ref() .or_else(|| self.album.as_ref().map(|a| &a.artist)) } /// Retourne le nom de l'album si disponible pub fn album_name(&self) -> Option<&str> { self.album.as_ref().map(|a| a.title.as_str()) } /// Vérifie si la piste est disponible pour le streaming pub fn is_available(&self) -> bool { self.streamable } } impl SearchResult { /// Crée un résultat de recherche vide pub fn new() -> Self { Self::default() } /// Retourne le nombre total de résultats pub fn total_count(&self) -> usize { self.albums.len() + self.artists.len() + self.tracks.len() + self.playlists.len() } /// Vérifie si la recherche n'a retourné aucun résultat pub fn is_empty(&self) -> bool { self.total_count() == 0 } } /// Types d'albums featured disponibles dans le catalogue Qobuz #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FeaturedAlbumType { /// Nouveautés NewReleases, /// Nouveautés complètes NewReleasesFull, /// Discographie idéale IdealDiscography, /// Qobuzissime Qobuzissims, /// Choix de l'éditeur EditorPicks, /// Prix de la presse PressAwards, } impl FeaturedAlbumType { /// Retourne l'identifiant API pour ce type pub fn api_id(&self) -> &'static str { match self { Self::NewReleases => "new-releases", Self::NewReleasesFull => "new-releases-full", Self::IdealDiscography => "ideal-discography", Self::Qobuzissims => "qobuzissims", Self::EditorPicks => "editor-picks", Self::PressAwards => "press-awards", } } } /// Tags de playlists disponibles dans le catalogue Qobuz #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PlaylistTag { /// Hi-Res HiRes, /// Nouvelles New, /// Thématiques Themes, /// Choix d'artistes ArtistsChoices, /// Labels Labels, /// Humeurs Moods, /// Artistes Artists, /// Événements Events, /// Auditoriums Auditoriums, /// Populaires Popular, } impl PlaylistTag { /// Retourne l'identifiant API pour ce tag pub fn api_id(&self) -> &'static str { match self { Self::HiRes => "hi-res", Self::New => "new", Self::Themes => "focus", Self::ArtistsChoices => "danslecasque", Self::Labels => "label", Self::Moods => "mood", Self::Artists => "artist", Self::Events => "events", Self::Auditoriums => "auditoriums", Self::Popular => "popular", } } /// Retourne le titre localisé pour ce tag pub fn display_name(&self) -> &'static str { match self { Self::HiRes => "Playlists (Hi-Res)", Self::New => "Playlists (New)", Self::Themes => "Playlists (Themes)", Self::ArtistsChoices => "Playlists (Artist's Choices)", Self::Labels => "Playlists (Labels)", Self::Moods => "Playlists (Moods)", Self::Artists => "Playlists (Artists)", Self::Events => "Playlists (Events)", Self::Auditoriums => "Playlists (Auditoriums)", Self::Popular => "Playlists (Popular)", } } /// Retourne la liste de tous les tags pub fn all() -> &'static [PlaylistTag] { &[ Self::HiRes, Self::New, Self::Themes, Self::ArtistsChoices, Self::Labels, Self::Moods, Self::Artists, Self::Events, Self::Auditoriums, Self::Popular, ] } }