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:
2 files changed
+35
No files matched your search
@@ -444,6 +444,19 @@ fn build_rediscover(
|
|||||||
/// Favorite Albums — the user's favorited albums, capped at 18, in favorite
|
/// Favorite Albums — the user's favorited albums, capped at 18, in favorite
|
||||||
/// order (matches Tauri's home-resolved favoriteAlbums sliced 18; unfiltered,
|
/// order (matches Tauri's home-resolved favoriteAlbums sliced 18; unfiltered,
|
||||||
/// unlike Rediscover).
|
/// 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> {
|
fn build_favorite_albums(fav_albums: &[Album]) -> Vec<AlbumCard> {
|
||||||
fav_albums.iter().take(18).cloned().map(map_album).collect()
|
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();
|
let recent_ids = recent_ids.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let fav_albums = fetch_fav_albums(&runtime).await;
|
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));
|
apply_favorite_albums(&weak, &cache, build_favorite_albums(&fav_albums));
|
||||||
// reco: backfill genres for the resolved favorite albums so the
|
// reco: backfill genres for the resolved favorite albums so the
|
||||||
// engine's top-genres has data (plays alone carry no genre).
|
// engine's top-genres has data (plays alone carry no genre).
|
||||||
|
|||||||
@@ -194,6 +194,24 @@ pub fn forgotten_favorite_album_ids(limit: u32, recency_days: u32) -> Option<Vec
|
|||||||
.ok()
|
.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 +
|
/// Backfill genres `(album_id, genre_id, genre_name)` onto reco events +
|
||||||
/// album-meta once albums are resolved. Best-effort, blocking SQLite — call
|
/// album-meta once albums are resolved. Best-effort, blocking SQLite — call
|
||||||
/// from `spawn_blocking`. Plays carry no genre, so this is what feeds
|
/// from `spawn_blocking`. Plays carry no genre, so this is what feeds
|
||||||
|
|||||||
Reference in new issue
Block a user