push-mvtuzlstzutl #35
@@ -1833,7 +1833,14 @@ impl ControlPoint {
|
||||
binding: Some(binding),
|
||||
});
|
||||
|
||||
let mut auto_start_cb = |rid: &RendererId| self.start_queue_playback_if_idle(rid);
|
||||
// For initial attach with auto_play, force playback start (don't check if idle)
|
||||
let mut auto_start_cb = |rid: &RendererId| {
|
||||
debug!(
|
||||
renderer = rid.0.as_str(),
|
||||
"Attach callback: forcing playback start (not checking if idle)"
|
||||
);
|
||||
self.play_current_from_queue(rid)
|
||||
};
|
||||
let callback: Option<&mut dyn FnMut(&RendererId) -> anyhow::Result<()>> = if auto_play {
|
||||
Some(&mut auto_start_cb)
|
||||
} else {
|
||||
@@ -2683,6 +2690,12 @@ fn refresh_attached_queue_for(
|
||||
|
||||
if auto_play {
|
||||
if let Some(callback) = after_refresh.as_deref_mut() {
|
||||
debug!(
|
||||
renderer = renderer_id.0.as_str(),
|
||||
server = server_id.0.as_str(),
|
||||
container = container_id.as_str(),
|
||||
"Auto-play enabled: calling callback to start playback"
|
||||
);
|
||||
if let Err(err) = callback(renderer_id) {
|
||||
warn!(
|
||||
renderer = renderer_id.0.as_str(),
|
||||
@@ -2691,8 +2704,25 @@ fn refresh_attached_queue_for(
|
||||
error = %err,
|
||||
"Failed to auto-start playback after playlist refresh"
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
renderer = renderer_id.0.as_str(),
|
||||
server = server_id.0.as_str(),
|
||||
container = container_id.as_str(),
|
||||
"Auto-play callback completed successfully"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
debug!(
|
||||
renderer = renderer_id.0.as_str(),
|
||||
"Auto-play enabled but no callback provided"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
debug!(
|
||||
renderer = renderer_id.0.as_str(),
|
||||
"Auto-play disabled, skipping playback start"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -22,7 +22,7 @@ use crate::{
|
||||
PlaybackPosition, PlaybackState, TransportControl, UpnpRenderer, VolumeControl,
|
||||
};
|
||||
use anyhow::{Result, anyhow};
|
||||
use tracing::warn;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
/// Backend-agnostic façade exposing transport, volume, and status contracts.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -281,8 +281,18 @@ impl MusicRenderer {
|
||||
pub fn clear_for_playlist_attach(&self) -> Result<()> {
|
||||
match self {
|
||||
MusicRenderer::OpenHome(_) => {
|
||||
// For OpenHome: clear the playlist on the renderer itself
|
||||
self.openhome_playlist_clear()
|
||||
// For OpenHome: clear the playlist (DeleteAll also stops playback automatically)
|
||||
// then explicitly stop to ensure clean state
|
||||
self.openhome_playlist_clear()?;
|
||||
self.stop().or_else(|err| -> Result<()> {
|
||||
// If stop fails (e.g., already stopped), that's fine
|
||||
warn!(
|
||||
renderer = self.id().0.as_str(),
|
||||
error = %err,
|
||||
"Stop failed after clearing OpenHome playlist (continuing anyway)"
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
MusicRenderer::Upnp(_)
|
||||
| MusicRenderer::Chromecast(_)
|
||||
@@ -436,21 +446,57 @@ impl MusicRenderer {
|
||||
// Get the current OpenHome playlist snapshot
|
||||
let snapshot = self.fetch_openhome_playlist_snapshot()?;
|
||||
|
||||
debug!(
|
||||
renderer = self.id().0.as_str(),
|
||||
tracks_count = snapshot.tracks.len(),
|
||||
current_id = ?snapshot.current_id,
|
||||
current_index = ?snapshot.current_index,
|
||||
"play_current_from_backend_queue: OpenHome snapshot fetched"
|
||||
);
|
||||
|
||||
if snapshot.tracks.is_empty() {
|
||||
return Err(anyhow!("OpenHome playlist is empty"));
|
||||
}
|
||||
|
||||
// Find the track_id to play (prefer current_id, then current_index, then first)
|
||||
let target_track_id = if let Some(current_id) = snapshot.current_id {
|
||||
debug!(
|
||||
renderer = self.id().0.as_str(),
|
||||
track_id = current_id,
|
||||
"Using current_id for playback"
|
||||
);
|
||||
Some(current_id)
|
||||
} else if let Some(current_idx) = snapshot.current_index {
|
||||
snapshot.tracks.get(current_idx).map(|track| track.id)
|
||||
let track_id = snapshot.tracks.get(current_idx).map(|track| track.id);
|
||||
debug!(
|
||||
renderer = self.id().0.as_str(),
|
||||
current_idx,
|
||||
track_id = ?track_id,
|
||||
"Using current_index for playback"
|
||||
);
|
||||
track_id
|
||||
} else {
|
||||
snapshot.tracks.first().map(|track| track.id)
|
||||
let track_id = snapshot.tracks.first().map(|track| track.id);
|
||||
debug!(
|
||||
renderer = self.id().0.as_str(),
|
||||
track_id = ?track_id,
|
||||
"Using first track for playback"
|
||||
);
|
||||
track_id
|
||||
};
|
||||
|
||||
if let Some(track_id) = target_track_id {
|
||||
info!(
|
||||
renderer = self.id().0.as_str(),
|
||||
track_id,
|
||||
"Calling openhome_playlist_play_id to start playback"
|
||||
);
|
||||
self.openhome_playlist_play_id(track_id)?;
|
||||
info!(
|
||||
renderer = self.id().0.as_str(),
|
||||
track_id,
|
||||
"Successfully called openhome_playlist_play_id"
|
||||
);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("No track to play in OpenHome playlist"))
|
||||
|
||||
@@ -135,6 +135,14 @@ impl RadioParadiseSource {
|
||||
let token = mgr.register_callback(move |event| {
|
||||
let pid = pid_clone.clone();
|
||||
if event.playlist_id == pid {
|
||||
// Ignorer PkUpdated - pas de notification UPnP (évite le reload côté control point)
|
||||
if matches!(event.kind, pmoplaylist::PlaylistEventKind::PkUpdated { .. }) {
|
||||
tracing::debug!(
|
||||
"PK swap in playlist {} - no UPnP notification (prevents playback restart)",
|
||||
event.playlist_id
|
||||
);
|
||||
return;
|
||||
}
|
||||
// On ne réagit qu'aux mises à jour structurelles (ajout/suppression)
|
||||
if !matches!(event.kind, pmoplaylist::PlaylistEventKind::Updated) {
|
||||
return;
|
||||
|
||||
@@ -527,7 +527,8 @@ impl WriteHandle {
|
||||
manager
|
||||
.rebuild_track_index(&self.playlist.id, &snapshot)
|
||||
.await;
|
||||
manager.notify_playlist_changed(&self.playlist.id);
|
||||
// PK swap uniquement - pas de notification UPnP pour éviter le reload
|
||||
manager.notify_playlist_pk_updated(&self.playlist.id, old_pk, new_pk);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -48,6 +48,9 @@ pub struct PlaylistEvent {
|
||||
pub enum PlaylistEventKind {
|
||||
/// La playlist a été modifiée (ajout/suppression/changement de config).
|
||||
Updated,
|
||||
/// Cache PK commuté (lazy→real) - pas de changement structurel.
|
||||
/// N'émet PAS de ContainersUpdated UPnP pour éviter le reload.
|
||||
PkUpdated { old_pk: String, new_pk: String },
|
||||
/// Un morceau référencé par la playlist a été servi par le cache audio.
|
||||
TrackPlayed { cache_pk: String, qualifier: String },
|
||||
}
|
||||
@@ -270,6 +273,18 @@ impl PlaylistManager {
|
||||
self.notify_playlist_event(id, PlaylistEventKind::Updated);
|
||||
}
|
||||
|
||||
/// Notifie que des PK ont été swappés (lazy→real).
|
||||
/// N'émet PAS de notification UPnP ContainersUpdated pour éviter le reload.
|
||||
pub(crate) fn notify_playlist_pk_updated(&self, id: &str, old_pk: &str, new_pk: &str) {
|
||||
self.notify_playlist_event(
|
||||
id,
|
||||
PlaylistEventKind::PkUpdated {
|
||||
old_pk: old_pk.to_string(),
|
||||
new_pk: new_pk.to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Notifie les callbacks qu'un morceau a été joué pour une playlist donnée.
|
||||
pub(crate) fn notify_playlist_track_played(
|
||||
&self,
|
||||
|
||||
@@ -55,6 +55,10 @@ pub async fn playlist_events_sse(Query(params): Query<EventsQuery>) -> impl Into
|
||||
|
||||
let (kind, cache_pk, qualifier) = match &envelope.event.kind {
|
||||
PlaylistEventKind::Updated => ("updated", None, None),
|
||||
PlaylistEventKind::PkUpdated { old_pk: _, new_pk: _ } => {
|
||||
// PK swap silencieux - envoyer quand même l'événement SSE pour debug/monitoring
|
||||
("pk_updated", None, None)
|
||||
}
|
||||
PlaylistEventKind::TrackPlayed { cache_pk, qualifier } => {
|
||||
("track_played", Some(cache_pk.as_str()), Some(qualifier.as_str()))
|
||||
}
|
||||
|
||||
@@ -149,6 +149,12 @@ struct FeaturedPlaylistsResponse {
|
||||
playlists: PaginatedResponse<PlaylistResponse>,
|
||||
}
|
||||
|
||||
/// Réponse artistes featured
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct FeaturedArtistsResponse {
|
||||
artists: PaginatedResponse<ArtistResponse>,
|
||||
}
|
||||
|
||||
/// Réponse search
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SearchResponse {
|
||||
@@ -397,6 +403,36 @@ impl QobuzApi {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Récupère les artistes featured
|
||||
pub async fn get_featured_artists(
|
||||
&self,
|
||||
genre_id: Option<&str>,
|
||||
limit: Option<u32>,
|
||||
offset: Option<u32>,
|
||||
) -> Result<Vec<Artist>> {
|
||||
debug!("Fetching featured artists");
|
||||
let limit_str = limit.unwrap_or(100).to_string();
|
||||
let offset_str = offset.unwrap_or(0).to_string();
|
||||
|
||||
let mut params = vec![
|
||||
("type", "featured-artists"),
|
||||
("limit", &limit_str),
|
||||
("offset", &offset_str),
|
||||
];
|
||||
|
||||
if let Some(gid) = genre_id {
|
||||
params.push(("genre_ids", gid));
|
||||
}
|
||||
|
||||
let response: FeaturedArtistsResponse = self.get("/artist/getFeatured", ¶ms).await?;
|
||||
Ok(response
|
||||
.artists
|
||||
.items
|
||||
.into_iter()
|
||||
.map(Self::parse_artist)
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Recherche dans le catalogue
|
||||
pub async fn search(&self, query: &str, type_: Option<&str>) -> Result<SearchResult> {
|
||||
debug!("Searching for '{}' (type: {:?})", query, type_);
|
||||
@@ -443,6 +479,13 @@ impl QobuzApi {
|
||||
// Fonctions de parsing publiques (utilisées aussi par le module user)
|
||||
|
||||
pub(crate) fn parse_album(response: AlbumResponse) -> Album {
|
||||
// Log pour débugger les valeurs audio
|
||||
if let Some(rate) = response.maximum_sampling_rate {
|
||||
debug!("Album {} - maximum_sampling_rate: {} Hz", response.id, rate);
|
||||
} else {
|
||||
debug!("Album {} - maximum_sampling_rate: None", response.id);
|
||||
}
|
||||
|
||||
Album {
|
||||
id: response.id,
|
||||
title: response.title,
|
||||
|
||||
@@ -752,6 +752,19 @@ impl QobuzClient {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Récupère les artistes featured
|
||||
pub async fn get_featured_artists(
|
||||
&self,
|
||||
genre_id: Option<&str>,
|
||||
limit: Option<u32>,
|
||||
offset: Option<u32>,
|
||||
) -> Result<Vec<Artist>> {
|
||||
self.call_with_auth_repair("get_featured_artists", || {
|
||||
self.api.get_featured_artists(genre_id, limit, offset)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
// ============ Recherche ============
|
||||
|
||||
/// Recherche dans le catalogue Qobuz
|
||||
|
||||
@@ -269,7 +269,13 @@ 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) {
|
||||
format!("{} ({:.0}/{} bit)", self.title, rate / 1000.0, 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()
|
||||
}
|
||||
@@ -316,3 +322,109 @@ impl SearchResult {
|
||||
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,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -721,13 +721,666 @@ impl QobuzSource {
|
||||
*last = SystemTime::now();
|
||||
}
|
||||
|
||||
/// Construit le container Discover Catalog
|
||||
fn build_discover_catalog_container(&self) -> Container {
|
||||
Container {
|
||||
id: "qobuz:discover".to_string(),
|
||||
parent_id: "qobuz".to_string(),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: "Discover Catalog".to_string(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Construit le container Discover Genres
|
||||
fn build_discover_genres_container(&self) -> Container {
|
||||
Container {
|
||||
id: "qobuz:genres".to_string(),
|
||||
parent_id: "qobuz".to_string(),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: "Discover Genres".to_string(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Construit le container Favourites (My Music)
|
||||
fn build_favourites_container(&self) -> Container {
|
||||
Container {
|
||||
id: "qobuz:favorites".to_string(),
|
||||
parent_id: "qobuz".to_string(),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: "My Music".to_string(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Construit le container Albums favoris
|
||||
fn build_favourite_albums_container(&self) -> Container {
|
||||
Container {
|
||||
id: "qobuz:favorites:albums".to_string(),
|
||||
parent_id: "qobuz:favorites".to_string(),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: "Albums".to_string(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Construit le container Tracks favoris
|
||||
fn build_favourite_tracks_container(&self) -> Container {
|
||||
Container {
|
||||
id: "qobuz:favorites:tracks".to_string(),
|
||||
parent_id: "qobuz:favorites".to_string(),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: "Tracks".to_string(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Construit le container Artists favoris
|
||||
fn build_favourite_artists_container(&self) -> Container {
|
||||
Container {
|
||||
id: "qobuz:favorites:artists".to_string(),
|
||||
parent_id: "qobuz:favorites".to_string(),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: "Artists".to_string(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Construit le container Playlists favoris
|
||||
fn build_favourite_playlists_container(&self) -> Container {
|
||||
Container {
|
||||
id: "qobuz:favorites:playlists".to_string(),
|
||||
parent_id: "qobuz:favorites".to_string(),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: "Playlists".to_string(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Browse Favourites - retourne 4 sous-containers
|
||||
async fn browse_favourites(&self) -> Result<BrowseResult> {
|
||||
let containers = vec![
|
||||
self.build_favourite_albums_container(),
|
||||
self.build_favourite_tracks_container(),
|
||||
self.build_favourite_artists_container(),
|
||||
self.build_favourite_playlists_container(),
|
||||
];
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
}
|
||||
|
||||
/// Browse Favourite Albums
|
||||
async fn browse_favourite_albums(&self) -> Result<BrowseResult> {
|
||||
let albums = self
|
||||
.inner
|
||||
.client
|
||||
.get_favorite_albums()
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
|
||||
let containers: Vec<Container> = albums
|
||||
.into_iter()
|
||||
.filter_map(|album| album.to_didl_container("qobuz:favorites:albums").ok())
|
||||
.collect();
|
||||
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
}
|
||||
|
||||
/// Browse Favourite Tracks
|
||||
async fn browse_favourite_tracks(&self) -> Result<BrowseResult> {
|
||||
let tracks = self
|
||||
.inner
|
||||
.client
|
||||
.get_favorite_tracks()
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
|
||||
let items: Vec<Item> = tracks
|
||||
.into_iter()
|
||||
.filter_map(|track| track.to_didl_item("qobuz:favorites:tracks").ok())
|
||||
.collect();
|
||||
|
||||
Ok(BrowseResult::Items(items))
|
||||
}
|
||||
|
||||
/// Browse Favourite Artists
|
||||
async fn browse_favourite_artists(&self) -> Result<BrowseResult> {
|
||||
let artists = self
|
||||
.inner
|
||||
.client
|
||||
.get_favorite_artists()
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
|
||||
let containers: Vec<Container> = artists
|
||||
.into_iter()
|
||||
.filter_map(|artist| {
|
||||
// Créer un container pour chaque artiste
|
||||
Some(Container {
|
||||
id: format!("qobuz:artist:{}", artist.id),
|
||||
parent_id: "qobuz:favorites:artists".to_string(),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: artist.name.clone(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
}
|
||||
|
||||
/// Browse Favourite Playlists
|
||||
async fn browse_favourite_playlists(&self) -> Result<BrowseResult> {
|
||||
let playlists = self
|
||||
.inner
|
||||
.client
|
||||
.get_user_playlists()
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
|
||||
let containers: Vec<Container> = playlists
|
||||
.into_iter()
|
||||
.filter_map(|playlist| playlist.to_didl_container("qobuz:favorites:playlists").ok())
|
||||
.collect();
|
||||
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
}
|
||||
|
||||
// ===== DISCOVER CATALOG =====
|
||||
|
||||
/// Browse Discover Catalog - retourne 5 containers principaux + 10 playlists par tag
|
||||
async fn browse_discover_catalog(&self) -> Result<BrowseResult> {
|
||||
use crate::models::PlaylistTag;
|
||||
|
||||
let mut containers = vec![
|
||||
self.build_discover_playlists_container(),
|
||||
self.build_discover_albums_ideal_container(),
|
||||
self.build_discover_albums_qobuzissime_container(),
|
||||
self.build_discover_albums_new_container(),
|
||||
self.build_discover_artists_container(),
|
||||
];
|
||||
|
||||
// Ajouter les 10 tags de playlists
|
||||
for tag in PlaylistTag::all() {
|
||||
containers.push(Container {
|
||||
id: format!("qobuz:discover:playlists:{}", tag.api_id()),
|
||||
parent_id: "qobuz:discover".to_string(),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: tag.display_name().to_string(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
}
|
||||
|
||||
fn build_discover_playlists_container(&self) -> Container {
|
||||
Container {
|
||||
id: "qobuz:discover:playlists".to_string(),
|
||||
parent_id: "qobuz:discover".to_string(),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: "Playlists".to_string(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn build_discover_albums_ideal_container(&self) -> Container {
|
||||
Container {
|
||||
id: "qobuz:discover:albums:ideal".to_string(),
|
||||
parent_id: "qobuz:discover".to_string(),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: "Albums (Ideal Discography)".to_string(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn build_discover_albums_qobuzissime_container(&self) -> Container {
|
||||
Container {
|
||||
id: "qobuz:discover:albums:qobuzissime".to_string(),
|
||||
parent_id: "qobuz:discover".to_string(),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: "Albums (Qobuzissime)".to_string(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn build_discover_albums_new_container(&self) -> Container {
|
||||
Container {
|
||||
id: "qobuz:discover:albums:new".to_string(),
|
||||
parent_id: "qobuz:discover".to_string(),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: "Albums (New Releases)".to_string(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn build_discover_artists_container(&self) -> Container {
|
||||
Container {
|
||||
id: "qobuz:discover:artists".to_string(),
|
||||
parent_id: "qobuz:discover".to_string(),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: "Artists".to_string(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Browse Discover Playlists (all featured playlists)
|
||||
async fn browse_discover_playlists(&self) -> Result<BrowseResult> {
|
||||
let playlists = self
|
||||
.inner
|
||||
.client
|
||||
.get_featured_playlists(None, None)
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
|
||||
let containers: Vec<Container> = playlists
|
||||
.into_iter()
|
||||
.filter_map(|playlist| playlist.to_didl_container("qobuz:discover:playlists").ok())
|
||||
.collect();
|
||||
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
}
|
||||
|
||||
/// Browse Discover Albums (Ideal Discography)
|
||||
async fn browse_discover_albums_ideal(&self) -> Result<BrowseResult> {
|
||||
let albums = self
|
||||
.inner
|
||||
.client
|
||||
.get_featured_albums(None, "ideal-discography")
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
|
||||
let containers: Vec<Container> = albums
|
||||
.into_iter()
|
||||
.filter_map(|album| album.to_didl_container("qobuz:discover:albums:ideal").ok())
|
||||
.collect();
|
||||
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
}
|
||||
|
||||
/// Browse Discover Albums (Qobuzissime)
|
||||
async fn browse_discover_albums_qobuzissime(&self) -> Result<BrowseResult> {
|
||||
let albums = self
|
||||
.inner
|
||||
.client
|
||||
.get_featured_albums(None, "qobuzissims")
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
|
||||
let containers: Vec<Container> = albums
|
||||
.into_iter()
|
||||
.filter_map(|album| album.to_didl_container("qobuz:discover:albums:qobuzissime").ok())
|
||||
.collect();
|
||||
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
}
|
||||
|
||||
/// Browse Discover Albums (New Releases)
|
||||
async fn browse_discover_albums_new(&self) -> Result<BrowseResult> {
|
||||
let albums = self
|
||||
.inner
|
||||
.client
|
||||
.get_featured_albums(None, "new-releases")
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
|
||||
let containers: Vec<Container> = albums
|
||||
.into_iter()
|
||||
.filter_map(|album| album.to_didl_container("qobuz:discover:albums:new").ok())
|
||||
.collect();
|
||||
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
}
|
||||
|
||||
/// Browse Discover Artists (Featured Artists)
|
||||
async fn browse_discover_artists(&self) -> Result<BrowseResult> {
|
||||
let artists = self
|
||||
.inner
|
||||
.client
|
||||
.get_featured_artists(None, None, None)
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
|
||||
let containers: Vec<Container> = artists
|
||||
.into_iter()
|
||||
.map(|artist| Container {
|
||||
id: format!("qobuz:artist:{}", artist.id),
|
||||
parent_id: "qobuz:discover:artists".to_string(),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: artist.name.clone(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
}
|
||||
|
||||
/// Browse Discover Playlists by tag
|
||||
async fn browse_discover_playlists_tag(&self, tag: &str) -> Result<BrowseResult> {
|
||||
let playlists = self
|
||||
.inner
|
||||
.client
|
||||
.get_featured_playlists(None, Some(tag))
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
|
||||
let parent_id = format!("qobuz:discover:playlists:{}", tag);
|
||||
let containers: Vec<Container> = playlists
|
||||
.into_iter()
|
||||
.filter_map(|playlist| playlist.to_didl_container(&parent_id).ok())
|
||||
.collect();
|
||||
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
}
|
||||
|
||||
// ===== DISCOVER GENRES =====
|
||||
|
||||
/// Browse Discover Genres - liste des genres
|
||||
async fn browse_discover_genres(&self) -> Result<BrowseResult> {
|
||||
let genres = self
|
||||
.inner
|
||||
.client
|
||||
.get_genres()
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
|
||||
let containers: Vec<Container> = genres
|
||||
.into_iter()
|
||||
.filter_map(|genre| {
|
||||
// Filtrer les genres sans ID
|
||||
genre.id.map(|id| Container {
|
||||
id: format!("qobuz:genre:{}", id),
|
||||
parent_id: "qobuz:genres".to_string(),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: genre.name.clone(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
}
|
||||
|
||||
/// Browse un genre spécifique - retourne 6 sous-containers
|
||||
async fn browse_genre(&self, genre_id: &str) -> Result<BrowseResult> {
|
||||
let containers = vec![
|
||||
self.build_genre_new_releases_container(genre_id),
|
||||
self.build_genre_ideal_discography_container(genre_id),
|
||||
self.build_genre_qobuzissime_container(genre_id),
|
||||
self.build_genre_editor_picks_container(genre_id),
|
||||
self.build_genre_press_awards_container(genre_id),
|
||||
self.build_genre_playlists_container(genre_id),
|
||||
];
|
||||
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
}
|
||||
|
||||
fn build_genre_new_releases_container(&self, genre_id: &str) -> Container {
|
||||
Container {
|
||||
id: format!("qobuz:genre:{}:new-releases", genre_id),
|
||||
parent_id: format!("qobuz:genre:{}", genre_id),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: "New Releases".to_string(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn build_genre_ideal_discography_container(&self, genre_id: &str) -> Container {
|
||||
Container {
|
||||
id: format!("qobuz:genre:{}:ideal", genre_id),
|
||||
parent_id: format!("qobuz:genre:{}", genre_id),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: "Ideal Discography".to_string(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn build_genre_qobuzissime_container(&self, genre_id: &str) -> Container {
|
||||
Container {
|
||||
id: format!("qobuz:genre:{}:qobuzissime", genre_id),
|
||||
parent_id: format!("qobuz:genre:{}", genre_id),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: "Qobuzissime".to_string(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn build_genre_editor_picks_container(&self, genre_id: &str) -> Container {
|
||||
Container {
|
||||
id: format!("qobuz:genre:{}:editor-picks", genre_id),
|
||||
parent_id: format!("qobuz:genre:{}", genre_id),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: "Editor Picks".to_string(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn build_genre_press_awards_container(&self, genre_id: &str) -> Container {
|
||||
Container {
|
||||
id: format!("qobuz:genre:{}:press-awards", genre_id),
|
||||
parent_id: format!("qobuz:genre:{}", genre_id),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: "Press Awards".to_string(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn build_genre_playlists_container(&self, genre_id: &str) -> Container {
|
||||
Container {
|
||||
id: format!("qobuz:genre:{}:playlists", genre_id),
|
||||
parent_id: format!("qobuz:genre:{}", genre_id),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None,
|
||||
searchable: Some("1".to_string()),
|
||||
title: "Qobuz Playlists".to_string(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Browse Genre New Releases
|
||||
async fn browse_genre_new_releases(&self, genre_id: &str) -> Result<BrowseResult> {
|
||||
let albums = self
|
||||
.inner
|
||||
.client
|
||||
.get_featured_albums(Some(genre_id), "new-releases")
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
|
||||
let parent_id = format!("qobuz:genre:{}:new-releases", genre_id);
|
||||
let containers: Vec<Container> = albums
|
||||
.into_iter()
|
||||
.filter_map(|album| album.to_didl_container(&parent_id).ok())
|
||||
.collect();
|
||||
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
}
|
||||
|
||||
/// Browse Genre Ideal Discography
|
||||
async fn browse_genre_ideal_discography(&self, genre_id: &str) -> Result<BrowseResult> {
|
||||
let albums = self
|
||||
.inner
|
||||
.client
|
||||
.get_featured_albums(Some(genre_id), "ideal-discography")
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
|
||||
let parent_id = format!("qobuz:genre:{}:ideal", genre_id);
|
||||
let containers: Vec<Container> = albums
|
||||
.into_iter()
|
||||
.filter_map(|album| album.to_didl_container(&parent_id).ok())
|
||||
.collect();
|
||||
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
}
|
||||
|
||||
/// Browse Genre Qobuzissime
|
||||
async fn browse_genre_qobuzissime(&self, genre_id: &str) -> Result<BrowseResult> {
|
||||
let albums = self
|
||||
.inner
|
||||
.client
|
||||
.get_featured_albums(Some(genre_id), "qobuzissims")
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
|
||||
let parent_id = format!("qobuz:genre:{}:qobuzissime", genre_id);
|
||||
let containers: Vec<Container> = albums
|
||||
.into_iter()
|
||||
.filter_map(|album| album.to_didl_container(&parent_id).ok())
|
||||
.collect();
|
||||
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
}
|
||||
|
||||
/// Browse Genre Editor Picks
|
||||
async fn browse_genre_editor_picks(&self, genre_id: &str) -> Result<BrowseResult> {
|
||||
let albums = self
|
||||
.inner
|
||||
.client
|
||||
.get_featured_albums(Some(genre_id), "editor-picks")
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
|
||||
let parent_id = format!("qobuz:genre:{}:editor-picks", genre_id);
|
||||
let containers: Vec<Container> = albums
|
||||
.into_iter()
|
||||
.filter_map(|album| album.to_didl_container(&parent_id).ok())
|
||||
.collect();
|
||||
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
}
|
||||
|
||||
/// Browse Genre Press Awards
|
||||
async fn browse_genre_press_awards(&self, genre_id: &str) -> Result<BrowseResult> {
|
||||
let albums = self
|
||||
.inner
|
||||
.client
|
||||
.get_featured_albums(Some(genre_id), "press-awards")
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
|
||||
let parent_id = format!("qobuz:genre:{}:press-awards", genre_id);
|
||||
let containers: Vec<Container> = albums
|
||||
.into_iter()
|
||||
.filter_map(|album| album.to_didl_container(&parent_id).ok())
|
||||
.collect();
|
||||
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
}
|
||||
|
||||
/// Browse Genre Playlists
|
||||
async fn browse_genre_playlists(&self, genre_id: &str) -> Result<BrowseResult> {
|
||||
let playlists = self
|
||||
.inner
|
||||
.client
|
||||
.get_featured_playlists(Some(genre_id), None)
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
|
||||
let parent_id = format!("qobuz:genre:{}:playlists", genre_id);
|
||||
let containers: Vec<Container> = playlists
|
||||
.into_iter()
|
||||
.filter_map(|playlist| playlist.to_didl_container(&parent_id).ok())
|
||||
.collect();
|
||||
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
}
|
||||
|
||||
/// Parse object_id to determine what to browse
|
||||
///
|
||||
/// Object IDs follow these patterns:
|
||||
/// - "qobuz" or "0" → Root container
|
||||
/// - "qobuz:favorites" → User's favorite albums
|
||||
/// - "qobuz:discover" → Discover Catalog
|
||||
/// - "qobuz:genres" → Discover Genres
|
||||
/// - "qobuz:favorites" → My Music
|
||||
/// - "qobuz:album:{id}" → Tracks in album
|
||||
/// - "qobuz:playlist:{id}" → Tracks in playlist
|
||||
/// - etc.
|
||||
fn parse_object_id(&self, object_id: &str) -> ObjectIdType {
|
||||
if object_id == "qobuz" || object_id == "0" {
|
||||
return ObjectIdType::Root;
|
||||
@@ -735,10 +1388,38 @@ impl QobuzSource {
|
||||
|
||||
let parts: Vec<&str> = object_id.split(':').collect();
|
||||
match parts.as_slice() {
|
||||
["qobuz", "favorites"] => ObjectIdType::Favorites,
|
||||
// Discover Catalog
|
||||
["qobuz", "discover"] => ObjectIdType::DiscoverCatalog,
|
||||
["qobuz", "discover", "playlists"] => ObjectIdType::DiscoverPlaylists,
|
||||
["qobuz", "discover", "albums", "ideal"] => ObjectIdType::DiscoverAlbumsIdeal,
|
||||
["qobuz", "discover", "albums", "qobuzissime"] => ObjectIdType::DiscoverAlbumsQobuzissime,
|
||||
["qobuz", "discover", "albums", "new"] => ObjectIdType::DiscoverAlbumsNew,
|
||||
["qobuz", "discover", "artists"] => ObjectIdType::DiscoverArtists,
|
||||
["qobuz", "discover", "playlists", tag] => ObjectIdType::DiscoverPlaylistsByTag(tag.to_string()),
|
||||
|
||||
// Discover Genres
|
||||
["qobuz", "genres"] => ObjectIdType::DiscoverGenres,
|
||||
["qobuz", "genre", id] => ObjectIdType::GenreRoot(id.to_string()),
|
||||
["qobuz", "genre", id, "new-releases"] => ObjectIdType::GenreNewReleases(id.to_string()),
|
||||
["qobuz", "genre", id, "ideal"] => ObjectIdType::GenreIdealDiscography(id.to_string()),
|
||||
["qobuz", "genre", id, "qobuzissime"] => ObjectIdType::GenreQobuzissime(id.to_string()),
|
||||
["qobuz", "genre", id, "editor-picks"] => ObjectIdType::GenreEditorPicks(id.to_string()),
|
||||
["qobuz", "genre", id, "press-awards"] => ObjectIdType::GenrePressAwards(id.to_string()),
|
||||
["qobuz", "genre", id, "playlists"] => ObjectIdType::GenrePlaylists(id.to_string()),
|
||||
|
||||
// Favourites
|
||||
["qobuz", "favorites"] => ObjectIdType::Favourites,
|
||||
["qobuz", "favorites", "albums"] => ObjectIdType::FavouriteAlbums,
|
||||
["qobuz", "favorites", "tracks"] => ObjectIdType::FavouriteTracks,
|
||||
["qobuz", "favorites", "artists"] => ObjectIdType::FavouriteArtists,
|
||||
["qobuz", "favorites", "playlists"] => ObjectIdType::FavouritePlaylists,
|
||||
|
||||
// Items (existant)
|
||||
["qobuz", "album", id] => ObjectIdType::Album(id.to_string()),
|
||||
["qobuz", "playlist", id] => ObjectIdType::Playlist(id.to_string()),
|
||||
["qobuz", "artist", id] => ObjectIdType::Artist(id.to_string()),
|
||||
["qobuz", "track", id] => ObjectIdType::Track(id.to_string()),
|
||||
|
||||
_ => ObjectIdType::Unknown,
|
||||
}
|
||||
}
|
||||
@@ -747,10 +1428,39 @@ impl QobuzSource {
|
||||
#[derive(Debug)]
|
||||
enum ObjectIdType {
|
||||
Root,
|
||||
Favorites,
|
||||
|
||||
// Discover Catalog
|
||||
DiscoverCatalog,
|
||||
DiscoverPlaylists,
|
||||
DiscoverAlbumsIdeal,
|
||||
DiscoverAlbumsQobuzissime,
|
||||
DiscoverAlbumsNew,
|
||||
DiscoverArtists,
|
||||
DiscoverPlaylistsByTag(String), // tag
|
||||
|
||||
// Discover Genres
|
||||
DiscoverGenres,
|
||||
GenreRoot(String), // genre_id
|
||||
GenreNewReleases(String), // genre_id
|
||||
GenreIdealDiscography(String), // genre_id
|
||||
GenreQobuzissime(String), // genre_id
|
||||
GenreEditorPicks(String), // genre_id
|
||||
GenrePressAwards(String), // genre_id
|
||||
GenrePlaylists(String), // genre_id
|
||||
|
||||
// Favourites
|
||||
Favourites,
|
||||
FavouriteAlbums,
|
||||
FavouriteTracks,
|
||||
FavouriteArtists,
|
||||
FavouritePlaylists,
|
||||
|
||||
// Items (existant)
|
||||
Album(String),
|
||||
Playlist(String),
|
||||
Artist(String),
|
||||
Track(String),
|
||||
|
||||
Unknown,
|
||||
}
|
||||
|
||||
@@ -769,28 +1479,19 @@ impl MusicSource for QobuzSource {
|
||||
}
|
||||
|
||||
async fn root_container(&self) -> Result<Container> {
|
||||
// Create the root container with sub-containers for different categories
|
||||
// Create the root container with 3 main branches
|
||||
Ok(Container {
|
||||
id: "qobuz".to_string(),
|
||||
parent_id: "0".to_string(),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: Some("2".to_string()), // Favorites + Search (simplified)
|
||||
child_count: Some("3".to_string()),
|
||||
searchable: Some("1".to_string()),
|
||||
title: "Qobuz".to_string(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![
|
||||
// Favorites container
|
||||
Container {
|
||||
id: "qobuz:favorites".to_string(),
|
||||
parent_id: "qobuz".to_string(),
|
||||
restricted: Some("1".to_string()),
|
||||
child_count: None, // Will be determined when browsed
|
||||
searchable: Some("1".to_string()),
|
||||
title: "My Favorites".to_string(),
|
||||
class: "object.container".to_string(),
|
||||
containers: vec![],
|
||||
items: vec![],
|
||||
},
|
||||
self.build_discover_catalog_container(),
|
||||
self.build_discover_genres_container(),
|
||||
self.build_favourites_container(),
|
||||
],
|
||||
items: vec![],
|
||||
})
|
||||
@@ -804,23 +1505,33 @@ impl MusicSource for QobuzSource {
|
||||
Ok(BrowseResult::Containers(root.containers))
|
||||
}
|
||||
|
||||
ObjectIdType::Favorites => {
|
||||
// Get user's favorite albums
|
||||
let albums = self
|
||||
.inner
|
||||
.client
|
||||
.get_favorite_albums()
|
||||
.await
|
||||
.map_err(|e| MusicSourceError::BrowseError(e.to_string()))?;
|
||||
// Discover Catalog
|
||||
ObjectIdType::DiscoverCatalog => self.browse_discover_catalog().await,
|
||||
ObjectIdType::DiscoverPlaylists => self.browse_discover_playlists().await,
|
||||
ObjectIdType::DiscoverAlbumsIdeal => self.browse_discover_albums_ideal().await,
|
||||
ObjectIdType::DiscoverAlbumsQobuzissime => self.browse_discover_albums_qobuzissime().await,
|
||||
ObjectIdType::DiscoverAlbumsNew => self.browse_discover_albums_new().await,
|
||||
ObjectIdType::DiscoverArtists => self.browse_discover_artists().await,
|
||||
ObjectIdType::DiscoverPlaylistsByTag(tag) => self.browse_discover_playlists_tag(&tag).await,
|
||||
|
||||
let containers: Vec<Container> = albums
|
||||
.into_iter()
|
||||
.filter_map(|album| album.to_didl_container("qobuz:favorites").ok())
|
||||
.collect();
|
||||
// Discover Genres
|
||||
ObjectIdType::DiscoverGenres => self.browse_discover_genres().await,
|
||||
ObjectIdType::GenreRoot(id) => self.browse_genre(&id).await,
|
||||
ObjectIdType::GenreNewReleases(id) => self.browse_genre_new_releases(&id).await,
|
||||
ObjectIdType::GenreIdealDiscography(id) => self.browse_genre_ideal_discography(&id).await,
|
||||
ObjectIdType::GenreQobuzissime(id) => self.browse_genre_qobuzissime(&id).await,
|
||||
ObjectIdType::GenreEditorPicks(id) => self.browse_genre_editor_picks(&id).await,
|
||||
ObjectIdType::GenrePressAwards(id) => self.browse_genre_press_awards(&id).await,
|
||||
ObjectIdType::GenrePlaylists(id) => self.browse_genre_playlists(&id).await,
|
||||
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
}
|
||||
// Favourites
|
||||
ObjectIdType::Favourites => self.browse_favourites().await,
|
||||
ObjectIdType::FavouriteAlbums => self.browse_favourite_albums().await,
|
||||
ObjectIdType::FavouriteTracks => self.browse_favourite_tracks().await,
|
||||
ObjectIdType::FavouriteArtists => self.browse_favourite_artists().await,
|
||||
ObjectIdType::FavouritePlaylists => self.browse_favourite_playlists().await,
|
||||
|
||||
// Items (existant)
|
||||
ObjectIdType::Album(album_id) => {
|
||||
let items = self
|
||||
.get_or_create_album_playlist_items(&album_id, usize::MAX)
|
||||
@@ -871,6 +1582,13 @@ impl MusicSource for QobuzSource {
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
}
|
||||
|
||||
ObjectIdType::Track(_) => {
|
||||
// Track object_ids ne sont pas browsables, retourner une erreur
|
||||
Err(MusicSourceError::NotSupported(
|
||||
"Tracks are not browsable containers".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
ObjectIdType::Unknown => Err(MusicSourceError::ObjectNotFound(object_id.to_string())),
|
||||
}
|
||||
}
|
||||
@@ -1242,7 +1960,7 @@ impl MusicSource for QobuzSource {
|
||||
|
||||
Ok(BrowseResult::Items(items))
|
||||
}
|
||||
ObjectIdType::Favorites => {
|
||||
ObjectIdType::FavouriteAlbums => {
|
||||
let albums = self
|
||||
.inner
|
||||
.client
|
||||
@@ -1254,7 +1972,7 @@ impl MusicSource for QobuzSource {
|
||||
.into_iter()
|
||||
.skip(offset)
|
||||
.take(limit)
|
||||
.filter_map(|album| album.to_didl_container("qobuz:favorites").ok())
|
||||
.filter_map(|album| album.to_didl_container("qobuz:favorites:albums").ok())
|
||||
.collect();
|
||||
|
||||
Ok(BrowseResult::Containers(containers))
|
||||
|
||||
Reference in New Issue
Block a user