Files
pmomusic/pmoqobuz/src/didl.rs
2025-10-29 22:35:53 +01:00

249 lines
8.1 KiB
Rust

//! Export des objets Qobuz en format DIDL-Lite
//!
//! Ce module permet de convertir les structures Qobuz (Album, Track, etc.)
//! en objets DIDL-Lite compatibles avec UPnP/DLNA.
use crate::error::{QobuzError, Result};
use crate::models::{Album, Playlist, Track};
use pmodidl::{Container, Item, Resource};
/// Trait pour convertir un objet Qobuz en DIDL-Lite
pub trait ToDIDL {
/// Convertit l'objet en Container DIDL
fn to_didl_container(&self, parent_id: &str) -> Result<Container>;
/// Convertit l'objet en Item DIDL
fn to_didl_item(&self, parent_id: &str) -> Result<Item>;
}
impl ToDIDL for Album {
/// Convertit un album en Container DIDL
///
/// # Arguments
///
/// * `parent_id` - ID du container parent
///
/// # Exemple
///
/// ```rust,ignore
/// let album = client.get_album("12345").await?;
/// let container = album.to_didl_container("0$qobuz$albums")?;
/// ```
fn to_didl_container(&self, parent_id: &str) -> Result<Container> {
let id = format!("0$qobuz$album${}", self.id);
Ok(Container {
id,
parent_id: parent_id.to_string(),
restricted: Some("1".to_string()),
child_count: self.tracks_count.map(|c| c.to_string()),
searchable: Some("1".to_string()),
title: self.formatted_title(),
class: "object.container.album.musicAlbum".to_string(),
containers: Vec::new(),
items: Vec::new(),
})
}
/// Un album ne peut pas être converti directement en Item
fn to_didl_item(&self, _parent_id: &str) -> Result<Item> {
Err(QobuzError::DidlExport(
"Album cannot be converted to Item, use to_didl_container instead".to_string(),
))
}
}
impl ToDIDL for Track {
/// Une track ne peut pas être convertie en Container
fn to_didl_container(&self, _parent_id: &str) -> Result<Container> {
Err(QobuzError::DidlExport(
"Track cannot be converted to Container, use to_didl_item instead".to_string(),
))
}
/// Convertit une track en Item DIDL
///
/// # Arguments
///
/// * `parent_id` - ID du container parent
///
/// # Exemple
///
/// ```rust,ignore
/// let track = client.get_track("98765").await?;
/// let item = track.to_didl_item("0$qobuz$album$12345")?;
/// ```
fn to_didl_item(&self, parent_id: &str) -> Result<Item> {
let id = format!("0$qobuz$track${}", self.id);
// Déterminer l'artiste à afficher
let artist_name = self
.display_artist()
.map(|a| a.name.clone())
.or_else(|| self.album.as_ref().map(|a| a.artist.name.clone()));
// Déterminer l'album
let album_name = self.album_name().map(|s| s.to_string());
// Déterminer l'image de couverture
let album_art = self
.album
.as_ref()
.and_then(|a| a.image_cached.clone().or_else(|| a.image.clone()));
// Créer la ressource (URL de streaming)
// Note: L'URL sera remplie plus tard via get_stream_url
let resource = Resource {
protocol_info: format!(
"http-get:*:{}:*",
self.mime_type.as_deref().unwrap_or("audio/flac")
),
bits_per_sample: self.bit_depth.map(|b| b.to_string()),
sample_frequency: self.sample_rate.map(|r| r.to_string()),
nr_audio_channels: self.channels.map(|c| c.to_string()),
duration: Some(format_duration(self.duration)),
url: format!("qobuz://track/{}", self.id), // URL symbolique
};
Ok(Item {
id,
parent_id: parent_id.to_string(),
restricted: Some("1".to_string()),
title: self.title.clone(),
creator: artist_name.clone(),
class: "object.item.audioItem.musicTrack".to_string(),
artist: artist_name,
album: album_name,
genre: None, // Qobuz ne fournit pas le genre au niveau track
album_art,
album_art_pk: None,
date: self.album.as_ref().and_then(|a| a.release_date.clone()),
original_track_number: Some(self.track_number.to_string()),
resources: vec![resource],
descriptions: Vec::new(),
})
}
}
impl ToDIDL for Playlist {
/// Convertit une playlist en Container DIDL
fn to_didl_container(&self, parent_id: &str) -> Result<Container> {
let id = format!("0$qobuz$playlist${}", self.id);
Ok(Container {
id,
parent_id: parent_id.to_string(),
restricted: Some("1".to_string()),
child_count: self.tracks_count.map(|c| c.to_string()),
searchable: Some("1".to_string()),
title: self.name.clone(),
class: "object.container.playlistContainer".to_string(),
containers: Vec::new(),
items: Vec::new(),
})
}
/// Une playlist ne peut pas être convertie en Item
fn to_didl_item(&self, _parent_id: &str) -> Result<Item> {
Err(QobuzError::DidlExport(
"Playlist cannot be converted to Item, use to_didl_container instead".to_string(),
))
}
}
/// Formate une durée en secondes au format HH:MM:SS
fn format_duration(seconds: u32) -> String {
let hours = seconds / 3600;
let minutes = (seconds % 3600) / 60;
let secs = seconds % 60;
format!("{:02}:{:02}:{:02}", hours, minutes, secs)
}
/// Convertit une liste de tracks en items DIDL
pub fn tracks_to_didl_items(tracks: &[Track], parent_id: &str) -> Result<Vec<Item>> {
tracks
.iter()
.map(|track| track.to_didl_item(parent_id))
.collect()
}
/// Convertit une liste d'albums en containers DIDL
pub fn albums_to_didl_containers(albums: &[Album], parent_id: &str) -> Result<Vec<Container>> {
albums
.iter()
.map(|album| album.to_didl_container(parent_id))
.collect()
}
/// Convertit une liste de playlists en containers DIDL
pub fn playlists_to_didl_containers(
playlists: &[Playlist],
parent_id: &str,
) -> Result<Vec<Container>> {
playlists
.iter()
.map(|playlist| playlist.to_didl_container(parent_id))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{Album, Artist, Track};
#[test]
fn test_album_to_didl_container() {
let album = Album {
id: "123".to_string(),
title: "Test Album".to_string(),
artist: Artist::new("456", "Test Artist"),
tracks_count: Some(10),
duration: Some(3000),
release_date: Some("2024-01-01".to_string()),
image: None,
image_cached: None,
streamable: true,
description: None,
maximum_sampling_rate: Some(96000.0),
maximum_bit_depth: Some(24),
genres: vec![],
label: None,
};
let container = album.to_didl_container("parent").unwrap();
assert_eq!(container.id, "0$qobuz$album$123");
assert_eq!(container.parent_id, "parent");
assert!(container.title.contains("Test Album"));
}
#[test]
fn test_track_to_didl_item() {
let track = Track {
id: "789".to_string(),
title: "Test Track".to_string(),
performer: Some(Artist::new("456", "Test Artist")),
album: None,
duration: 180,
track_number: 1,
media_number: 1,
streamable: true,
mime_type: Some("audio/flac".to_string()),
sample_rate: Some(44100),
bit_depth: Some(16),
channels: Some(2),
};
let item = track.to_didl_item("parent").unwrap();
assert_eq!(item.id, "0$qobuz$track$789");
assert_eq!(item.parent_id, "parent");
assert_eq!(item.title, "Test Track");
}
#[test]
fn test_format_duration() {
assert_eq!(format_duration(0), "00:00:00");
assert_eq!(format_duration(90), "00:01:30");
assert_eq!(format_duration(3665), "01:01:05");
}
}