feat(reco): augment discovery known-artists with reco signal

Add RecoStore::get_known_artist_ids (artists played >threshold OR favorited;
unit-tested) and union it into the network-discovery 'skip artists I already
know' filter. play_history stays the source of the (id, name) known set:
reco_events has no artist names and the schema is frozen to match Tauri, so
full consolidation (plan D2) would regress the name-based MB candidate filter.
This augments the id set with reco's richer plays+favorites signal instead.

cc
This commit is contained in:
vicrodh committed 2026-06-26 20:03:19 -06:00
1 parent c6fb6953bf
commit 25ab1408e5
3 files changed
+81 -1

No files matched your search

+59
View File
@@ -554,6 +554,37 @@ impl RecoStore {
Ok(artists)
}
/// Distinct artist ids the user "knows": played strictly more than
/// `play_threshold` times OR favorited at least once. Backs the discovery
/// "skip artists I already know" filter with a richer signal (plays +
/// favorites) than the play-only `play_history` store.
pub fn get_known_artist_ids(
&self,
play_threshold: u32,
) -> Result<std::collections::HashSet<u64>, String> {
let mut stmt = self
.conn
.prepare(
r#"
SELECT artist_id
FROM reco_events
WHERE artist_id IS NOT NULL
GROUP BY artist_id
HAVING SUM(CASE WHEN event_type = 'play' THEN 1 ELSE 0 END) > ?
OR SUM(CASE WHEN event_type = 'favorite' THEN 1 ELSE 0 END) > 0
"#,
)
.map_err(|e| format!("Failed to prepare known artists query: {}", e))?;
let rows = stmt
.query_map(params![play_threshold], |row| row.get::<_, u64>(0))
.map_err(|e| format!("Failed to query known artists: {}", e))?;
let mut ids = std::collections::HashSet::new();
for row in rows {
ids.insert(row.map_err(|e| format!("Failed to read known artist row: {}", e))?);
}
Ok(ids)
}
/// The user's most-played genres by event count (mirrors `get_top_genre_ids`).
/// Returns `(genre_id, genre_name)` — name from `reco_album_meta` (empty if unknown).
pub fn get_top_genres(&self, limit: u32) -> Result<Vec<(u64, String)>, String> {
@@ -1277,4 +1308,32 @@ mod tests {
assert_eq!(genres[1].0, 6);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn known_artists_includes_played_and_favorited() {
let dir = unique_test_dir("reco-known-artists");
let store = RecoStore::new_at(&dir).unwrap();
// artist 10: 3 plays (> threshold 2). artist 20: a single favorite, no
// plays. artist 30: 1 play, not favorited -> excluded.
store.log_play_event(1, Some("a".into()), Some(10), None).unwrap();
store.log_play_event(2, Some("a".into()), Some(10), None).unwrap();
store.log_play_event(3, Some("a".into()), Some(10), None).unwrap();
store
.insert_event(&RecoEventInput {
event_type: RecoEventType::Favorite,
item_type: RecoItemType::Artist,
track_id: None,
album_id: None,
artist_id: Some(20),
playlist_id: None,
genre_id: None,
})
.unwrap();
store.log_play_event(4, Some("c".into()), Some(30), None).unwrap();
let known = store.get_known_artist_ids(2).unwrap();
assert!(known.contains(&10)); // 3 plays > 2
assert!(known.contains(&20)); // favorited
assert!(!known.contains(&30)); // 1 play, not favorited
let _ = std::fs::remove_dir_all(dir);
}
}
+11 -1
View File
@@ -1540,7 +1540,17 @@ where
seed_name,
&similar_names,
&|tag| crate::discovery_dismiss::dismissed_for_tag(tag),
&|| crate::play_history::known_artists(known_threshold),
&|| {
// play_history supplies the (id, name) known set; reco augments
// the id set with its richer signal (artists played >threshold
// OR favorited). Names stay from play_history -- reco_events has
// no artist names (schema frozen to match Tauri).
let (mut ids, names) = crate::play_history::known_artists(known_threshold);
if let Some(reco_ids) = crate::reco::known_artist_ids(known_threshold) {
ids.extend(reco_ids);
}
(ids, names)
},
)
.await
.map_err(|e| e.to_string())?;
+11
View File
@@ -12,6 +12,7 @@
//! file is shared with Tauri (`<base_dir>/reco/events.db`), so a user's
//! existing recommendation history carries across frontends.
use std::collections::HashSet;
use std::path::Path;
use std::sync::Mutex;
@@ -230,6 +231,16 @@ pub fn backfill_album_genres(entries: Vec<(String, u64, String)>) {
}
}
/// Artist ids the user "knows" — played more than `play_threshold` times OR
/// favorited — to augment the discovery "skip artists I already know" filter
/// with the reco signal (plays + favorites, not plays alone). `None` when reco
/// is cold/disabled, so discovery falls back to the play_history set only.
pub fn known_artist_ids(play_threshold: u32) -> Option<HashSet<u64>> {
let guard = RECO.lock().ok()?;
let store = guard.as_ref()?;
store.get_known_artist_ids(play_threshold).ok()
}
// ---------------------------------------------------------------------------
// Training (W5)
// ---------------------------------------------------------------------------