Fix bug play single track Qobuz
Corrige le bug empêchant la lecture d'un track Qobuz individuel depuis le ServerDrawer. - Implémente `get_item()` dans `QobuzSource` pour retourner des URLs HTTP valides au lieu de URLs symboliques - Rend `format_duration()` publique dans `pmoqobuz/src/didl.rs` - Ajoute le logging dans `pmocontrol/src/pmoserver_ext.rs` pour suivre le flux de lecture - Améliore la gestion des objets dans `pmocontrol/src/pmoserver_ext.rs` pour distinguer les conteneurs des éléments
This commit is contained in:
41
Blackboard/Done/bug_play_single_track.md
Normal file
41
Blackboard/Done/bug_play_single_track.md
Normal file
@@ -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é.
|
||||||
37
Blackboard/Report/bug_play_single_track.md
Normal file
37
Blackboard/Report/bug_play_single_track.md
Normal file
@@ -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é.
|
||||||
@@ -1599,9 +1599,22 @@ async fn play_content(
|
|||||||
// The UI will be updated via SSE events when playback starts
|
// The UI will be updated via SSE events when playback starts
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
let result = tokio::task::spawn_blocking(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
|
// Fetch playback items from server
|
||||||
let items = fetch_playback_items(&control_point, &sid, &object_id)?;
|
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() {
|
if items.is_empty() {
|
||||||
return Err(anyhow::anyhow!("No playable content found"));
|
return Err(anyhow::anyhow!("No playable content found"));
|
||||||
}
|
}
|
||||||
@@ -2144,8 +2157,16 @@ fn fetch_playback_items(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Browse the object to get entries
|
// First, get metadata for the object to determine if it's a container or item
|
||||||
let entries = server.browse_children(object_id, 0, BROWSE_PAGE_SIZE)?;
|
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!(
|
debug!(
|
||||||
server_id = server_id.0.as_str(),
|
server_id = server_id.0.as_str(),
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ impl ToDIDL for Playlist {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Formate une durée en secondes au format HH:MM:SS
|
/// 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 hours = seconds / 3600;
|
||||||
let minutes = (seconds % 3600) / 60;
|
let minutes = (seconds % 3600) / 60;
|
||||||
let secs = seconds % 60;
|
let secs = seconds % 60;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
//! providing a complete music catalog browsing and searching experience.
|
//! providing a complete music catalog browsing and searching experience.
|
||||||
|
|
||||||
use crate::client::QobuzClient;
|
use crate::client::QobuzClient;
|
||||||
use crate::didl::ToDIDL;
|
use crate::didl::{format_duration, ToDIDL};
|
||||||
use crate::lazy_provider::QobuzLazyProvider;
|
use crate::lazy_provider::QobuzLazyProvider;
|
||||||
use crate::models::Track;
|
use crate::models::Track;
|
||||||
use pmoaudiocache::{AudioMetadata, Cache as AudioCache};
|
use pmoaudiocache::{AudioMetadata, Cache as AudioCache};
|
||||||
@@ -1690,6 +1690,88 @@ impl MusicSource for QobuzSource {
|
|||||||
.map_err(|e| MusicSourceError::UriResolutionError(e.to_string()))
|
.map_err(|e| MusicSourceError::UriResolutionError(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_item(&self, object_id: &str) -> Result<Item> {
|
||||||
|
// 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 {
|
fn supports_fifo(&self) -> bool {
|
||||||
// Qobuz is a catalog, not a dynamic stream
|
// Qobuz is a catalog, not a dynamic stream
|
||||||
false
|
false
|
||||||
|
|||||||
Reference in New Issue
Block a user