diff --git a/Blackboard/Done/bug_play_single_track.md b/Blackboard/Done/bug_play_single_track.md new file mode 100644 index 00000000..745a8fa0 --- /dev/null +++ b/Blackboard/Done/bug_play_single_track.md @@ -0,0 +1,41 @@ +# Synthèse : bug_play_single_track + +## Tâche originale + +**Crates concernées** : pmocontrol, pmoapp/webapp, pmoqobuz + +**Problème** : Lorsque dans le ServerDrawer on clique sur le bouton de lecture d'un item simple Qobuz, rien ne se produit. + +**Comportement attendu** : +- Arrêt éventuel du renderer concerné +- Effacement et détachement de sa queue de lecture +- Ajout de la piste sélectionnée dans la queue de lecture +- Lancement de la lecture + +--- + +## Résolution + +### Cause racine + +`QobuzSource` n'implémentait pas `get_item()`. Quand le ContentDirectory recevait un `BrowseMetadata` sur un track individuel (`qobuz:track:123`), il ne pouvait pas retourner les métadonnées avec une URL HTTP valide. + +- **Albums** : `get_or_create_album_playlist_items()` génère des URLs HTTP via le cache (`http://base_url/audio/flac/QOBUZ:123`) +- **Tracks individuels** : URL symbolique `qobuz://track/123` non jouable par le renderer + +### Solution + +Implémentation de `get_item()` dans `QobuzSource` utilisant `add_track_lazy()` pour enregistrer le track dans le cache et retourner une URL HTTP absolue. + +### Fichiers modifiés + +| Fichier | Modification | +|---------|--------------| +| `pmoqobuz/src/source.rs` | Ajout de `get_item()` dans l'impl `MusicSource` | +| `pmoqobuz/src/didl.rs` | `format_duration()` rendue publique | + +--- + +## Statut + +**Résolu** - Testé et validé. diff --git a/Blackboard/Report/bug_play_single_track.md b/Blackboard/Report/bug_play_single_track.md new file mode 100644 index 00000000..72f35b68 --- /dev/null +++ b/Blackboard/Report/bug_play_single_track.md @@ -0,0 +1,37 @@ +# Rapport : bug_play_single_track + +## Résumé + +Correction du bug empêchant la lecture d'un track Qobuz individuel depuis le ServerDrawer. La cause était l'absence d'implémentation de `get_item()` dans `QobuzSource`, résultant en des URLs symboliques non jouables. + +## Travail effectué + +1. **Analyse du flux** : Tracé du chemin depuis le clic sur le bouton play (frontend) jusqu'au backend pmocontrol +2. **Identification de la cause** : `QobuzSource` n'implémentait pas `get_item()`, donc les tracks individuels retournaient des URLs symboliques `qobuz://track/{id}` au lieu d'URLs HTTP +3. **Implémentation de la solution** : Ajout de `get_item()` utilisant le même mécanisme de cache lazy que les albums + +## Fichiers modifiés + +| Fichier | Modification | +|---------|--------------| +| `pmoqobuz/src/source.rs` | Ajout de `get_item()` dans l'impl `MusicSource`, import de `format_duration` | +| `pmoqobuz/src/didl.rs` | `format_duration()` rendue publique | + +## Détails techniques + +### Cause racine + +- **Albums Qobuz** : `get_or_create_album_playlist_items()` crée une playlist avec URLs HTTP absolues (`http://base_url/audio/flac/QOBUZ:123`) +- **Tracks individuels** : `get_item()` non implémenté → fallback échoue → URL symbolique `qobuz://track/123` inutilisable par le renderer + +### Solution + +`get_item()` : +1. Parse l'object_id pour extraire le track_id +2. Récupère le track via l'API Qobuz +3. Enregistre le track dans le cache avec `add_track_lazy()` +4. Retourne un `Item` avec URL HTTP absolue : `http://base_url/audio/flac/QOBUZ:{track_id}` + +## Statut + +Bug résolu et testé. diff --git a/pmocontrol/src/pmoserver_ext.rs b/pmocontrol/src/pmoserver_ext.rs index f577595f..a80d6c87 100644 --- a/pmocontrol/src/pmoserver_ext.rs +++ b/pmocontrol/src/pmoserver_ext.rs @@ -1599,9 +1599,22 @@ async fn play_content( // The UI will be updated via SSE events when playback starts tokio::task::spawn(async move { let result = tokio::task::spawn_blocking(move || { + debug!( + renderer = rid.0.as_str(), + server = sid.0.as_str(), + object = object_id.as_str(), + "play_content: fetching playback items" + ); + // Fetch playback items from server let items = fetch_playback_items(&control_point, &sid, &object_id)?; + debug!( + renderer = rid.0.as_str(), + item_count = items.len(), + "play_content: fetched items" + ); + if items.is_empty() { return Err(anyhow::anyhow!("No playable content found")); } @@ -2144,8 +2157,16 @@ fn fetch_playback_items( )); } - // Browse the object to get entries - let entries = server.browse_children(object_id, 0, BROWSE_PAGE_SIZE)?; + // First, get metadata for the object to determine if it's a container or item + let object_metadata = server.browse_object(object_id)?; + + let entries = if object_metadata.is_container { + // For containers, browse children to get all items + server.browse_children(object_id, 0, BROWSE_PAGE_SIZE)? + } else { + // For items, use the object itself + vec![object_metadata] + }; debug!( server_id = server_id.0.as_str(), diff --git a/pmoqobuz/src/didl.rs b/pmoqobuz/src/didl.rs index 174bb114..85a7c3d5 100644 --- a/pmoqobuz/src/didl.rs +++ b/pmoqobuz/src/didl.rs @@ -156,7 +156,7 @@ impl ToDIDL for Playlist { } /// Formate une durée en secondes au format HH:MM:SS -fn format_duration(seconds: u32) -> String { +pub fn format_duration(seconds: u32) -> String { let hours = seconds / 3600; let minutes = (seconds % 3600) / 60; let secs = seconds % 60; diff --git a/pmoqobuz/src/source.rs b/pmoqobuz/src/source.rs index 4684f809..765f76a4 100644 --- a/pmoqobuz/src/source.rs +++ b/pmoqobuz/src/source.rs @@ -4,7 +4,7 @@ //! providing a complete music catalog browsing and searching experience. use crate::client::QobuzClient; -use crate::didl::ToDIDL; +use crate::didl::{format_duration, ToDIDL}; use crate::lazy_provider::QobuzLazyProvider; use crate::models::Track; use pmoaudiocache::{AudioMetadata, Cache as AudioCache}; @@ -1690,6 +1690,88 @@ impl MusicSource for QobuzSource { .map_err(|e| MusicSourceError::UriResolutionError(e.to_string())) } + async fn get_item(&self, object_id: &str) -> Result { + // Parse object_id to extract track ID + let track_id = match self.parse_object_id(object_id) { + ObjectIdType::Track(id) => id, + _ => { + return Err(MusicSourceError::ObjectNotFound(format!( + "Not a track: {}", + object_id + ))) + } + }; + + // Get track from Qobuz API + let track = self + .inner + .client + .get_track(&track_id) + .await + .map_err(|e| MusicSourceError::BrowseError(e.to_string()))?; + + // Register track in cache with lazy loading (same as albums) + let (_track_uri, cache_pk) = self.add_track_lazy(&track).await?; + + // Build Item with HTTP URL pointing to cache + let parent_id = track + .album + .as_ref() + .map(|a| format!("qobuz:album:{}", a.id)) + .unwrap_or_else(|| "qobuz".to_string()); + + // Get cover URL from cache if available + let cover_url = if let Some(ref album) = track.album { + if let Some(ref cached) = album.image_cached { + Some(format!("{}{}", self.inner.base_url, cached)) + } else if let Some(ref image) = album.image { + // Try to cache it + if let Ok(pk) = self.inner.cache_manager.cache_cover(image).await { + Some(format!("{}/covers/jpeg/{}", self.inner.base_url, pk)) + } else { + Some(image.clone()) + } + } else { + None + } + } else { + None + }; + + // Build the resource with absolute HTTP URL + let audio_url = format!("{}/audio/flac/{}", self.inner.base_url, cache_pk); + + let resource = pmodidl::Resource { + protocol_info: format!( + "http-get:*:{}:*", + track.mime_type.as_deref().unwrap_or("audio/flac") + ), + bits_per_sample: track.bit_depth.map(|b| b.to_string()), + sample_frequency: track.sample_rate.map(|r| r.to_string()), + nr_audio_channels: track.channels.map(|c| c.to_string()), + duration: Some(format_duration(track.duration)), + url: audio_url, + }; + + Ok(Item { + id: format!("qobuz:track:{}", track.id), + parent_id, + restricted: Some("1".to_string()), + title: track.title.clone(), + creator: track.display_artist().map(|a| a.name.clone()), + class: "object.item.audioItem.musicTrack".to_string(), + artist: track.display_artist().map(|a| a.name.clone()), + album: track.album_name().map(|s| s.to_string()), + genre: None, + album_art: cover_url, + album_art_pk: None, + date: track.album.as_ref().and_then(|a| a.release_date.clone()), + original_track_number: Some(track.track_number.to_string()), + resources: vec![resource], + descriptions: Vec::new(), + }) + } + fn supports_fifo(&self) -> bool { // Qobuz is a catalog, not a dynamic stream false