feat(slint, core): per-artist play history feeds the discovery filter
Ports the minimum of src-tauri/src/reco_store needed to honour the
"skip artists I already listen to" gate in the discovery pipeline.
play_history (`crates/qbz-slint/src/play_history.rs`):
- Tiny SQLite store at <data-dir>/qbz/play_history.db with WAL
(ADR-002), holding play_events (artist_id + occurred_at, one row
per audible track start) and artist_names (id -> name upsert).
Lazy open, errors collapsed to log::warn so a missing DB just
means an empty filter — same default Tauri lands on a fresh
profile.
- record_play(artist_id, name) wired into playback::record_recent
so every audible track start increments the per-artist count.
- known_artists(threshold) returns the (HashSet<u64>,
HashSet<String>) pair Tauri's filter expects — qobuz ids for
suppressing validated matches, normalized names for suppressing
MB candidates upstream.
qbz-core::musicbrainz_discover_artists:
- New `known_artists` closure parameter, called once per
invocation. Threshold matches Tauri (>2 plays).
- Filter applied at both stages: MB candidates dropped when their
normalized name is in known_names; Qobuz matches dropped when
their id is in known_ids (mirrors local_known_qobuz_ids check
in v2_get_discovery_artists).
The Tauri reco_store has many other features (favorites, top
genres, home seeds) the Slint shell does not use yet — those stay
out of this commit; only the play-count surface needed by the
sidebar's "You may also like" filter is ported.
cc
This commit is contained in:
7 files changed
+191
-2
No files matched your search
@@ -1584,6 +1584,12 @@ impl<A: FrontendAdapter + Send + Sync + 'static> QbzCore<A> {
|
||||
seed_name: &str,
|
||||
similar_names: &[String],
|
||||
dismissed_per_tag: &(dyn Fn(&str) -> std::collections::HashSet<String> + Send + Sync),
|
||||
known_artists: &(dyn Fn() -> (
|
||||
std::collections::HashSet<u64>,
|
||||
std::collections::HashSet<String>,
|
||||
)
|
||||
+ Send
|
||||
+ Sync),
|
||||
) -> Result<DiscoveryResponse, CoreError> {
|
||||
use std::collections::HashSet;
|
||||
|
||||
@@ -1624,6 +1630,7 @@ impl<A: FrontendAdapter + Send + Sync + 'static> QbzCore<A> {
|
||||
let similar_norm: HashSet<String> =
|
||||
similar_names.iter().map(|n| normalize_artist_name(n)).collect();
|
||||
let dismissed_primary = dismissed_per_tag(&primary_tag.to_lowercase());
|
||||
let (known_ids, known_names) = known_artists();
|
||||
|
||||
let mut candidates: Vec<(String, String)> = Vec::new();
|
||||
for artist in &mb_results.artists {
|
||||
@@ -1632,6 +1639,7 @@ impl<A: FrontendAdapter + Send + Sync + 'static> QbzCore<A> {
|
||||
|| artist.id.eq_ignore_ascii_case(seed_mbid)
|
||||
|| similar_norm.contains(&n)
|
||||
|| dismissed_primary.contains(&n)
|
||||
|| known_names.contains(&n)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -1643,7 +1651,7 @@ impl<A: FrontendAdapter + Send + Sync + 'static> QbzCore<A> {
|
||||
let max_results = 8;
|
||||
let min_results = 5;
|
||||
let mut results = self
|
||||
.validate_discovery_on_qobuz(&candidates, max_results)
|
||||
.validate_discovery_on_qobuz(&candidates, max_results, &known_ids)
|
||||
.await;
|
||||
|
||||
if results.len() < min_results && seed_tags.len() > 1 {
|
||||
@@ -1664,6 +1672,7 @@ impl<A: FrontendAdapter + Send + Sync + 'static> QbzCore<A> {
|
||||
|| similar_norm.contains(&n)
|
||||
|| dismissed_primary.contains(&n)
|
||||
|| dismissed_secondary.contains(&n)
|
||||
|| known_names.contains(&n)
|
||||
|| existing_mbids.contains(&a.id)
|
||||
{
|
||||
continue;
|
||||
@@ -1673,7 +1682,11 @@ impl<A: FrontendAdapter + Send + Sync + 'static> QbzCore<A> {
|
||||
shuffle_with_seed(&mut secondary_candidates, seed_mbid, Some(&secondary_tag));
|
||||
let remaining = max_results.saturating_sub(results.len());
|
||||
let mut more = self
|
||||
.validate_discovery_on_qobuz(&secondary_candidates, remaining)
|
||||
.validate_discovery_on_qobuz(
|
||||
&secondary_candidates,
|
||||
remaining,
|
||||
&known_ids,
|
||||
)
|
||||
.await;
|
||||
results.append(&mut more);
|
||||
}
|
||||
@@ -1689,6 +1702,7 @@ impl<A: FrontendAdapter + Send + Sync + 'static> QbzCore<A> {
|
||||
&self,
|
||||
candidates: &[(String, String)],
|
||||
max: usize,
|
||||
known_ids: &std::collections::HashSet<u64>,
|
||||
) -> Vec<DiscoveryArtist> {
|
||||
let mut out: Vec<DiscoveryArtist> = Vec::new();
|
||||
for (mbid, name) in candidates {
|
||||
@@ -1704,6 +1718,12 @@ impl<A: FrontendAdapter + Send + Sync + 'static> QbzCore<A> {
|
||||
if normalize_artist_name(&first.name) != normalize_artist_name(name) {
|
||||
continue;
|
||||
}
|
||||
// Tauri's `!local_known_qobuz_ids.contains(&artist.id)`
|
||||
// gate — never suggest an artist the user has already
|
||||
// listened to >2 times.
|
||||
if known_ids.contains(&first.id) {
|
||||
continue;
|
||||
}
|
||||
out.push(DiscoveryArtist {
|
||||
mbid: mbid.clone(),
|
||||
name: first.name.clone(),
|
||||
|
||||
Reference in new issue
Block a user