feat(reco): genre backfill + reco-backed Rediscover row
Wire the reco store into the For You favorite-albums branch: - Genre backfill: when favorite albums resolve (full Album with genre), write each (genre_id, genre_name) onto its reco events + album-meta via spawn_blocking. Plays carry no genre, so this makes the engine's get_top_genres non-empty (Tauri never backfilled, so its top-genres was dead). - Rediscover: build_rediscover prefers the reco 'forgotten favorites' set (favorited, not played in 30d, from the Tauri-shared events.db) when warm, falling back to the local recents heuristic when cold so the row never empties (non-regressing for fresh stores, better for Tauri-migrants). cc
This commit is contained in:
2 files changed
+74
-4
No files matched your search
@@ -420,11 +420,21 @@ fn top_artist_slims(fav_artists: &[Artist]) -> Vec<ArtistSlim> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rediscover — favorite albums not in the recent play-history.
|
/// Rediscover — favorite albums the user hasn't returned to lately. Prefers the
|
||||||
fn build_rediscover(fav_albums: &[Album], recent_ids: &HashSet<String>) -> Vec<AlbumCard> {
|
/// reco store's "forgotten favorites" (favorited, not played in 30d, from the
|
||||||
|
/// Tauri-shared events.db) when warm; falls back to "not in the local recents
|
||||||
|
/// cache" when reco is cold so the row never empties.
|
||||||
|
fn build_rediscover(
|
||||||
|
fav_albums: &[Album],
|
||||||
|
recent_ids: &HashSet<String>,
|
||||||
|
forgotten: Option<&HashSet<String>>,
|
||||||
|
) -> Vec<AlbumCard> {
|
||||||
fav_albums
|
fav_albums
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|a| !recent_ids.contains(&a.id))
|
.filter(|a| match forgotten {
|
||||||
|
Some(set) => set.contains(&a.id),
|
||||||
|
None => !recent_ids.contains(&a.id),
|
||||||
|
})
|
||||||
.take(18)
|
.take(18)
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(map_album)
|
.map(map_album)
|
||||||
@@ -854,7 +864,34 @@ pub fn spawn_for_you<A>(
|
|||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let fav_albums = fetch_fav_albums(&runtime).await;
|
let fav_albums = fetch_fav_albums(&runtime).await;
|
||||||
apply_favorite_albums(&weak, &cache, build_favorite_albums(&fav_albums));
|
apply_favorite_albums(&weak, &cache, build_favorite_albums(&fav_albums));
|
||||||
apply_rediscover(&weak, &cache, build_rediscover(&fav_albums, &recent_ids));
|
// reco: backfill genres for the resolved favorite albums so the
|
||||||
|
// engine's top-genres has data (plays alone carry no genre).
|
||||||
|
let genre_entries: Vec<(String, u64, String)> = fav_albums
|
||||||
|
.iter()
|
||||||
|
.filter_map(|a| {
|
||||||
|
a.genre
|
||||||
|
.as_ref()
|
||||||
|
.filter(|g| g.id > 0)
|
||||||
|
.map(|g| (a.id.clone(), g.id, g.name.clone()))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if !genre_entries.is_empty() {
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
crate::reco::backfill_album_genres(genre_entries)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// reco: prefer the reco "forgotten favorites" set when the store
|
||||||
|
// is warm (shared events.db); fall back to the local recents
|
||||||
|
// heuristic when cold so the Rediscover row never empties.
|
||||||
|
let forgotten: Option<HashSet<String>> =
|
||||||
|
crate::reco::forgotten_favorite_album_ids(60, 30)
|
||||||
|
.filter(|ids| !ids.is_empty())
|
||||||
|
.map(|ids| ids.into_iter().collect());
|
||||||
|
apply_rediscover(
|
||||||
|
&weak,
|
||||||
|
&cache,
|
||||||
|
build_rediscover(&fav_albums, &recent_ids, forgotten.as_ref()),
|
||||||
|
);
|
||||||
apply_radio(&weak, &cache, build_radio(&recent_album_list, &fav_albums));
|
apply_radio(&weak, &cache, build_radio(&recent_album_list, &fav_albums));
|
||||||
|
|
||||||
// Only the no-recent-history case needs the favorite-album seed;
|
// Only the no-recent-history case needs the favorite-album seed;
|
||||||
|
|||||||
@@ -179,6 +179,39 @@ pub fn log_playlist_add(playlist_id: Option<u64>, track_ids: Vec<u64>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Surfaces (W7/W8)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Forgotten-favorite album ids (favorited, not played within `recency_days`).
|
||||||
|
/// `None` when reco is disabled or the read fails — the caller falls back to
|
||||||
|
/// its local heuristic so the Rediscover row never empties on a cold store.
|
||||||
|
pub fn forgotten_favorite_album_ids(limit: u32, recency_days: u32) -> Option<Vec<String>> {
|
||||||
|
let guard = RECO.lock().ok()?;
|
||||||
|
let store = guard.as_ref()?;
|
||||||
|
store
|
||||||
|
.get_forgotten_favorite_album_ids(limit, recency_days)
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// `get_top_genres`; idempotent (only fills still-NULL event genres).
|
||||||
|
pub fn backfill_album_genres(entries: Vec<(String, u64, String)>) {
|
||||||
|
if entries.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Ok(guard) = RECO.lock() {
|
||||||
|
if let Some(store) = guard.as_ref() {
|
||||||
|
for (album_id, genre_id, genre_name) in entries {
|
||||||
|
let _ = store.update_genre_for_album(&album_id, genre_id);
|
||||||
|
let _ = store.set_album_genre_name(&album_id, &genre_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Training (W5)
|
// Training (W5)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in new issue
Block a user