feat(reco): order For You favorites by trained taste score

Reorder the resolved favorite albums so the reco-scored favorites lead (taste
order from the trained favorite_album scores), keeping unscored albums in their
original relative order. When reco is cold/disabled the row keeps the original
Qobuz order, so a fresh store never regresses. The reorder flows into the
Favorites, Rediscover, Radio and more-from-library seed (all share fav_albums).

cc
This commit is contained in:
vicrodh committed 2026-06-26 20:00:39 -06:00
1 parent e97d43d61a
commit c6fb6953bf
2 files changed
+35

No files matched your search

+17
View File
@@ -444,6 +444,19 @@ fn build_rediscover(
/// Favorite Albums — the user's favorited albums, capped at 18, in favorite
/// order (matches Tauri's home-resolved favoriteAlbums sliced 18; unfiltered,
/// unlike Rediscover).
/// Reorder resolved albums so the reco-scored favorites lead (taste order),
/// keeping unscored albums in their original relative order. `scored` is the
/// reco favorite-album id order; `None`/empty leaves the input untouched, so a
/// cold reco store never reorders (no regression).
fn order_by_score(mut albums: Vec<Album>, scored: Option<&[String]>) -> Vec<Album> {
if let Some(order) = scored {
if !order.is_empty() {
albums.sort_by_key(|a| order.iter().position(|id| id == &a.id).unwrap_or(usize::MAX));
}
}
albums
}
fn build_favorite_albums(fav_albums: &[Album]) -> Vec<AlbumCard> {
fav_albums.iter().take(18).cloned().map(map_album).collect()
}
@@ -863,6 +876,10 @@ pub fn spawn_for_you<A>(
let recent_ids = recent_ids.clone();
Box::pin(async move {
let fav_albums = fetch_fav_albums(&runtime).await;
// reco: lead with the highest-scored favorites (trained taste
// order) when the store is warm; cold -> original Qobuz order.
let scored_fav = crate::reco::scored_favorite_album_ids(80);
let fav_albums = order_by_score(fav_albums, scored_fav.as_deref());
apply_favorite_albums(&weak, &cache, build_favorite_albums(&fav_albums));
// reco: backfill genres for the resolved favorite albums so the
// engine's top-genres has data (plays alone carry no genre).
+18
View File
@@ -194,6 +194,24 @@ pub fn forgotten_favorite_album_ids(limit: u32, recency_days: u32) -> Option<Vec
.ok()
}
/// The reco-scored favorite album ids in taste order (highest first) when the
/// store is warm (trained); `None` when reco is cold/disabled so the caller
/// keeps its original ordering. Bounded by `limit`.
pub fn scored_favorite_album_ids(limit: u32) -> Option<Vec<String>> {
let limits = HomeSeedLimits {
recent_albums: 0,
continue_tracks: 0,
top_artists: 0,
favorites: limit,
};
let seeds = home_seeds(limits)?;
if seeds.favorite_album_ids.is_empty() {
None
} else {
Some(seeds.favorite_album_ids)
}
}
/// Backfill genres `(album_id, genre_id, genre_name)` onto reco events +
/// album-meta once albums are resolved. Best-effort, blocking SQLite — call
/// from `spawn_blocking`. Plays carry no genre, so this is what feeds