feat(discover): add get_dynamic_suggest_full carrying track_to_analysed

Tauri's DailyQ/WeeklyQ /dynamic/suggest seeds a track_to_analysed payload
(~9 resolved {track_id,artist_id,genre_id,label_id} tuples) for the PRIMARY
mix algorithm; the empty-analysis call is only its fallback. The Slint port
only ever sent the empty-analysis path. Add a TrackToAnalyse model
(qbz-models) and a get_dynamic_suggest_full method on the qobuz client and
qbz-core that carries the payload; the existing get_dynamic_suggest now
delegates with an empty analysis (behaviour unchanged). Plumbing only — the
mix.rs build of the analysis tuples + the two-call fallback lands next (a2).

cc
This commit is contained in:
vicrodh committed 2026-06-15 15:50:41 -06:00
1 parent 76406aa746
commit ee83734fba
4 files changed
+47 -3

No files matched your search

+18 -2
View File
@@ -12,8 +12,8 @@ use qbz_models::{
LabelListPage, LabelPageData, LabelStoryResponse, PageArtistResponse, LabelListPage, LabelPageData, LabelStoryResponse, PageArtistResponse,
MostPopularItem, Playlist, PlaylistDuplicateResult, PlaylistTag, Quality, QueueState, MostPopularItem, Playlist, PlaylistDuplicateResult, PlaylistTag, Quality, QueueState,
QueueTrack, ReleasesGridResponse, QueueTrack, ReleasesGridResponse,
RepeatMode, SearchAllResults, SearchResultsPage, StreamUrl, Track, TracksContainer, RepeatMode, SearchAllResults, SearchResultsPage, StreamUrl, Track, TrackToAnalyse,
UserSession, TracksContainer, UserSession,
}; };
use qbz_integrations::musicbrainz::cache::MusicBrainzCache; use qbz_integrations::musicbrainz::cache::MusicBrainzCache;
use qbz_integrations::musicbrainz::genre::{extract_affinity_seeds, genre_summary, is_broad_genre}; use qbz_integrations::musicbrainz::genre::{extract_affinity_seeds, genre_summary, is_broad_genre};
@@ -1319,6 +1319,22 @@ impl<A: FrontendAdapter + Send + Sync + 'static> QbzCore<A> {
.map_err(CoreError::Api) .map_err(CoreError::Api)
} }
/// Dynamic mix suggestions with the `track_to_analysed` payload — the
/// PRIMARY DailyQ/WeeklyQ path (see `QobuzClient::get_dynamic_suggest_full`).
pub async fn get_dynamic_suggest_full(
&self,
listened_track_ids: &[u64],
tracks_to_analyse: &[TrackToAnalyse],
limit: u32,
) -> Result<Vec<Track>, CoreError> {
let client = self.client.read().await;
let client = client.as_ref().ok_or(CoreError::NotInitialized)?;
client
.get_dynamic_suggest_full(listened_track_ids, tracks_to_analyse, limit)
.await
.map_err(CoreError::Api)
}
/// Smart artist radio via the local `qbz-radio` pool builder — a /// Smart artist radio via the local `qbz-radio` pool builder — a
/// richer alternative to the Qobuz `/radio/artist` endpoint. Builds /// richer alternative to the Qobuz `/radio/artist` endpoint. Builds
/// a session pool (seed tracks + similar artists + second-degree) /// a session pool (seed tracks + similar artists + second-degree)
+1
View File
@@ -124,6 +124,7 @@ pub use types::{
StreamUrl, StreamUrl,
Track, Track,
TrackFileUrl, TrackFileUrl,
TrackToAnalyse,
TracksContainer, TracksContainer,
UserSession, UserSession,
}; };
+13
View File
@@ -9,6 +9,19 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashSet; use std::collections::HashSet;
// ============ Dynamic-suggest (DailyQ/WeeklyQ) ============
/// A seed track resolved for the `/dynamic/suggest` `track_to_analysed`
/// payload (DailyQ / WeeklyQ). Field names match the Qobuz wire shape
/// exactly; `0` marks an unknown id (mirrors Tauri's `?? 0`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrackToAnalyse {
pub track_id: u64,
pub artist_id: u64,
pub genre_id: u64,
pub label_id: u64,
}
// ============ Quality Types ============ // ============ Quality Types ============
/// Audio quality format IDs (matches Qobuz API format IDs) /// Audio quality format IDs (matches Qobuz API format IDs)
+15 -1
View File
@@ -985,12 +985,26 @@ impl QobuzClient {
&self, &self,
listened_track_ids: &[u64], listened_track_ids: &[u64],
limit: u32, limit: u32,
) -> Result<Vec<Track>> {
self.get_dynamic_suggest_full(listened_track_ids, &[], limit)
.await
}
/// Like [`get_dynamic_suggest`] but carrying the `track_to_analysed`
/// payload — the PRIMARY DailyQ/WeeklyQ path. Tauri seeds this with up to 9
/// resolved `{track_id, artist_id, genre_id, label_id}` tuples and only
/// falls back to an empty analysis when a call returns zero items.
pub async fn get_dynamic_suggest_full(
&self,
listened_track_ids: &[u64],
tracks_to_analyse: &[TrackToAnalyse],
limit: u32,
) -> Result<Vec<Track>> { ) -> Result<Vec<Track>> {
let url = endpoints::build_url(paths::DYNAMIC_SUGGEST); let url = endpoints::build_url(paths::DYNAMIC_SUGGEST);
let body = serde_json::json!({ let body = serde_json::json!({
"limit": limit, "limit": limit,
"listened_tracks_ids": listened_track_ids, "listened_tracks_ids": listened_track_ids,
"track_to_analysed": [], "track_to_analysed": tracks_to_analyse,
}); });
let http_response = self let http_response = self
.http()? .http()?