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:
3 files changed
+81
-1
No files matched your search
@@ -554,6 +554,37 @@ impl RecoStore {
|
|||||||
Ok(artists)
|
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`).
|
/// 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).
|
/// 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> {
|
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);
|
assert_eq!(genres[1].0, 6);
|
||||||
let _ = std::fs::remove_dir_all(dir);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1540,7 +1540,17 @@ where
|
|||||||
seed_name,
|
seed_name,
|
||||||
&similar_names,
|
&similar_names,
|
||||||
&|tag| crate::discovery_dismiss::dismissed_for_tag(tag),
|
&|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
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
//! file is shared with Tauri (`<base_dir>/reco/events.db`), so a user's
|
//! file is shared with Tauri (`<base_dir>/reco/events.db`), so a user's
|
||||||
//! existing recommendation history carries across frontends.
|
//! existing recommendation history carries across frontends.
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Mutex;
|
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)
|
// Training (W5)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in new issue
Block a user