feat(vectors): add vector builder with MB and Qobuz sources
- Add weights.rs with configurable RelationshipWeights - Add builder.rs with ArtistVectorBuilder for constructing vectors - Integrate MusicBrainz relationships (members, groups, collaborators) - Integrate Qobuz similar artists API - Add ensure_vector() with TTL for lazy building - Add resolve_qobuz_to_mbid() for ID resolution via name search - 21 tests passing (sparse_vector, store, weights, builder)
This commit is contained in:
1 parent
1d2e28c1a8
commit
f3dcb6ebf2
4 files changed
+582
-3
No files matched your search
@@ -0,0 +1,374 @@
|
|||||||
|
//! Vector builder - constructs artist vectors from MusicBrainz and Qobuz data
|
||||||
|
//!
|
||||||
|
//! This module integrates with MusicBrainz for relationship data and Qobuz
|
||||||
|
//! for similarity data to build sparse vectors for each artist.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::api::QobuzClient;
|
||||||
|
use crate::musicbrainz::models::{
|
||||||
|
ArtistFullResponse, ArtistRelationships, Period, RelatedArtist,
|
||||||
|
};
|
||||||
|
use crate::musicbrainz::{MusicBrainzCache, MusicBrainzClient};
|
||||||
|
|
||||||
|
use super::sparse_vector::SparseVector;
|
||||||
|
use super::store::ArtistVectorStore;
|
||||||
|
use super::weights::RelationshipWeights;
|
||||||
|
|
||||||
|
/// Builder for constructing artist vectors from multiple data sources
|
||||||
|
pub struct ArtistVectorBuilder {
|
||||||
|
/// Vector store for persistence
|
||||||
|
store: Arc<Mutex<ArtistVectorStore>>,
|
||||||
|
/// MusicBrainz client for relationship data
|
||||||
|
mb_client: Arc<MusicBrainzClient>,
|
||||||
|
/// MusicBrainz cache
|
||||||
|
mb_cache: Arc<Mutex<MusicBrainzCache>>,
|
||||||
|
/// Qobuz client for similar artists
|
||||||
|
qobuz_client: Arc<Mutex<QobuzClient>>,
|
||||||
|
/// Configurable weights
|
||||||
|
weights: RelationshipWeights,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of building a vector
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BuildResult {
|
||||||
|
/// The built vector
|
||||||
|
pub vector: SparseVector,
|
||||||
|
/// Number of MusicBrainz relationships found
|
||||||
|
pub mb_relations_count: usize,
|
||||||
|
/// Number of Qobuz similar artists found
|
||||||
|
pub qobuz_similar_count: usize,
|
||||||
|
/// Sources that contributed to the vector
|
||||||
|
pub sources: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ArtistVectorBuilder {
|
||||||
|
/// Create a new builder with the given dependencies
|
||||||
|
pub fn new(
|
||||||
|
store: Arc<Mutex<ArtistVectorStore>>,
|
||||||
|
mb_client: Arc<MusicBrainzClient>,
|
||||||
|
mb_cache: Arc<Mutex<MusicBrainzCache>>,
|
||||||
|
qobuz_client: Arc<Mutex<QobuzClient>>,
|
||||||
|
weights: RelationshipWeights,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
store,
|
||||||
|
mb_client,
|
||||||
|
mb_cache,
|
||||||
|
qobuz_client,
|
||||||
|
weights,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a vector for an artist, fetching data from all sources
|
||||||
|
///
|
||||||
|
/// This method:
|
||||||
|
/// 1. Fetches MusicBrainz relationships (members, groups, collaborators)
|
||||||
|
/// 2. Fetches Qobuz similar artists (if Qobuz ID available)
|
||||||
|
/// 3. Combines all data into a weighted sparse vector
|
||||||
|
/// 4. Persists the vector to the store
|
||||||
|
pub async fn build_vector(
|
||||||
|
&self,
|
||||||
|
artist_mbid: &str,
|
||||||
|
artist_name: Option<&str>,
|
||||||
|
qobuz_artist_id: Option<u64>,
|
||||||
|
) -> Result<BuildResult, String> {
|
||||||
|
let mut vector = SparseVector::new();
|
||||||
|
let mut sources = Vec::new();
|
||||||
|
let mut mb_relations_count = 0;
|
||||||
|
let mut qobuz_similar_count = 0;
|
||||||
|
|
||||||
|
// 1. Get or create index for this artist
|
||||||
|
{
|
||||||
|
let mut store = self.store.lock().await;
|
||||||
|
store.get_or_create_idx(artist_mbid, artist_name)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fetch MusicBrainz relationships
|
||||||
|
match self.build_from_musicbrainz(artist_mbid).await {
|
||||||
|
Ok((mb_vec, count)) => {
|
||||||
|
vector = vector.add(&mb_vec);
|
||||||
|
mb_relations_count = count;
|
||||||
|
if count > 0 {
|
||||||
|
sources.push("musicbrainz".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to fetch MusicBrainz relations for {}: {}", artist_mbid, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fetch Qobuz similar artists (if we have the ID)
|
||||||
|
if let Some(qobuz_id) = qobuz_artist_id {
|
||||||
|
match self.build_from_qobuz(qobuz_id).await {
|
||||||
|
Ok((qobuz_vec, count)) => {
|
||||||
|
vector = vector.add(&qobuz_vec);
|
||||||
|
qobuz_similar_count = count;
|
||||||
|
if count > 0 {
|
||||||
|
sources.push("qobuz".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to fetch Qobuz similar for {}: {}", qobuz_id, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Persist the vector
|
||||||
|
{
|
||||||
|
let mut store = self.store.lock().await;
|
||||||
|
|
||||||
|
// Store MB relationships
|
||||||
|
if mb_relations_count > 0 {
|
||||||
|
// We need to rebuild just the MB part for proper source tagging
|
||||||
|
if let Ok((mb_vec, _)) = self.build_from_musicbrainz(artist_mbid).await {
|
||||||
|
store.set_vector(artist_mbid, &mb_vec, "musicbrainz")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store Qobuz similarities
|
||||||
|
if qobuz_similar_count > 0 {
|
||||||
|
if let Some(qobuz_id) = qobuz_artist_id {
|
||||||
|
if let Ok((qobuz_vec, _)) = self.build_from_qobuz(qobuz_id).await {
|
||||||
|
store.set_vector(artist_mbid, &qobuz_vec, "qobuz")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(BuildResult {
|
||||||
|
vector,
|
||||||
|
mb_relations_count,
|
||||||
|
qobuz_similar_count,
|
||||||
|
sources,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build vector component from MusicBrainz relationships
|
||||||
|
async fn build_from_musicbrainz(&self, artist_mbid: &str) -> Result<(SparseVector, usize), String> {
|
||||||
|
// Try cache first
|
||||||
|
let cached = {
|
||||||
|
let cache = self.mb_cache.lock().await;
|
||||||
|
cache.get_artist_relations(artist_mbid)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let relations = if let Some(rel) = cached {
|
||||||
|
rel
|
||||||
|
} else {
|
||||||
|
// Fetch from API
|
||||||
|
let response = self.mb_client.get_artist_with_relations(artist_mbid).await?;
|
||||||
|
|
||||||
|
// Extract relationships from raw response
|
||||||
|
let extracted = extract_relationships(&response);
|
||||||
|
|
||||||
|
// Cache it
|
||||||
|
{
|
||||||
|
let cache = self.mb_cache.lock().await;
|
||||||
|
cache.set_artist_relations(artist_mbid, &extracted)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
extracted
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut vector = SparseVector::new();
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
// Get store for index lookups
|
||||||
|
let mut store = self.store.lock().await;
|
||||||
|
|
||||||
|
// Process members (band → person)
|
||||||
|
for member in &relations.members {
|
||||||
|
let idx = store.get_or_create_idx(&member.mbid, Some(&member.name))?;
|
||||||
|
let weight = self.weights.member_of_band;
|
||||||
|
vector.set(idx, weight);
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process past members (slightly lower weight)
|
||||||
|
for member in &relations.past_members {
|
||||||
|
let idx = store.get_or_create_idx(&member.mbid, Some(&member.name))?;
|
||||||
|
let weight = self.weights.member_of_band * 0.8; // Past members slightly less relevant
|
||||||
|
vector.set(idx, weight);
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process groups (person → band)
|
||||||
|
for group in &relations.groups {
|
||||||
|
let idx = store.get_or_create_idx(&group.mbid, Some(&group.name))?;
|
||||||
|
let weight = self.weights.has_member;
|
||||||
|
vector.set(idx, weight);
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process collaborators
|
||||||
|
for collab in &relations.collaborators {
|
||||||
|
let idx = store.get_or_create_idx(&collab.mbid, Some(&collab.name))?;
|
||||||
|
let weight = self.weights.collaboration;
|
||||||
|
vector.set(idx, weight);
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((vector, count))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build vector component from Qobuz similar artists
|
||||||
|
async fn build_from_qobuz(&self, qobuz_artist_id: u64) -> Result<(SparseVector, usize), String> {
|
||||||
|
let similar = {
|
||||||
|
let client = self.qobuz_client.lock().await;
|
||||||
|
client
|
||||||
|
.get_similar_artists(qobuz_artist_id, 20, 0)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Qobuz API error: {}", e))?
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut vector = SparseVector::new();
|
||||||
|
let mut count = 0;
|
||||||
|
let mut store = self.store.lock().await;
|
||||||
|
|
||||||
|
for artist in similar.items {
|
||||||
|
// We need to resolve Qobuz artist to MBID
|
||||||
|
// For now, use a synthetic MBID based on Qobuz ID (we'll improve this later)
|
||||||
|
let synthetic_mbid = format!("qobuz:{}", artist.id);
|
||||||
|
|
||||||
|
let idx = store.get_or_create_idx(&synthetic_mbid, Some(&artist.name))?;
|
||||||
|
let weight = self.weights.qobuz_similar;
|
||||||
|
vector.set(idx, weight);
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((vector, count))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure a vector exists and is fresh, building if necessary
|
||||||
|
///
|
||||||
|
/// Returns true if vector was built/updated, false if existing vector was used
|
||||||
|
pub async fn ensure_vector(
|
||||||
|
&self,
|
||||||
|
artist_mbid: &str,
|
||||||
|
artist_name: Option<&str>,
|
||||||
|
qobuz_artist_id: Option<u64>,
|
||||||
|
max_age_days: i64,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
let max_age_secs = max_age_days * 24 * 60 * 60;
|
||||||
|
|
||||||
|
// Check if we have a fresh vector
|
||||||
|
let has_fresh = {
|
||||||
|
let store = self.store.lock().await;
|
||||||
|
store.has_fresh_vector(artist_mbid, max_age_secs)
|
||||||
|
};
|
||||||
|
|
||||||
|
if has_fresh {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build new vector
|
||||||
|
self.build_vector(artist_mbid, artist_name, qobuz_artist_id).await?;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a Qobuz artist ID to a MusicBrainz ID by searching
|
||||||
|
///
|
||||||
|
/// This is a best-effort match based on artist name
|
||||||
|
pub async fn resolve_qobuz_to_mbid(
|
||||||
|
qobuz_client: &Arc<Mutex<QobuzClient>>,
|
||||||
|
mb_client: &Arc<MusicBrainzClient>,
|
||||||
|
_mb_cache: &Arc<Mutex<MusicBrainzCache>>,
|
||||||
|
qobuz_artist_id: u64,
|
||||||
|
) -> Result<Option<String>, String> {
|
||||||
|
// Get artist name from Qobuz
|
||||||
|
let artist_name = {
|
||||||
|
let client = qobuz_client.lock().await;
|
||||||
|
let artist = client
|
||||||
|
.get_artist(qobuz_artist_id, false)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to get Qobuz artist: {}", e))?;
|
||||||
|
artist.name
|
||||||
|
};
|
||||||
|
|
||||||
|
// Search MusicBrainz for this artist
|
||||||
|
let mb_result = mb_client.search_artist(&artist_name).await?;
|
||||||
|
|
||||||
|
// Return first high-confidence match (score >= 80)
|
||||||
|
if let Some(artist) = mb_result.artists.first() {
|
||||||
|
if let Some(score) = artist.score {
|
||||||
|
if score >= 80 {
|
||||||
|
return Ok(Some(artist.id.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract ArtistRelationships from raw MusicBrainz response
|
||||||
|
fn extract_relationships(artist: &ArtistFullResponse) -> ArtistRelationships {
|
||||||
|
let mut members = Vec::new();
|
||||||
|
let mut past_members = Vec::new();
|
||||||
|
let mut groups = Vec::new();
|
||||||
|
let mut collaborators = Vec::new();
|
||||||
|
|
||||||
|
if let Some(relations) = &artist.relations {
|
||||||
|
for relation in relations {
|
||||||
|
let Some(related_artist) = &relation.artist else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let related = RelatedArtist {
|
||||||
|
mbid: related_artist.id.clone(),
|
||||||
|
name: related_artist.name.clone(),
|
||||||
|
role: relation.attributes.as_ref().and_then(|a| a.first().cloned()),
|
||||||
|
period: Some(Period {
|
||||||
|
begin: relation.begin.clone(),
|
||||||
|
end: relation.end.clone(),
|
||||||
|
}),
|
||||||
|
ended: relation.ended.unwrap_or(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
match relation.relation_type.as_str() {
|
||||||
|
"member of band" => {
|
||||||
|
if relation.direction.as_deref() == Some("backward") {
|
||||||
|
// We're viewing a BAND, the related artist is a MEMBER
|
||||||
|
if related.ended {
|
||||||
|
past_members.push(related);
|
||||||
|
} else {
|
||||||
|
members.push(related);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We're viewing a PERSON, the related artist is a BAND/GROUP
|
||||||
|
groups.push(related);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"collaboration" => {
|
||||||
|
collaborators.push(related);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtistRelationships {
|
||||||
|
members,
|
||||||
|
past_members,
|
||||||
|
groups,
|
||||||
|
collaborators,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// Integration tests would require mocking the clients
|
||||||
|
// For now, we test the weight application logic
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_weights_applied() {
|
||||||
|
let weights = RelationshipWeights::default();
|
||||||
|
|
||||||
|
assert!(weights.member_of_band > weights.collaboration);
|
||||||
|
assert!(weights.collaboration > weights.shared_tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,8 +43,12 @@
|
|||||||
//! ...]
|
//! ...]
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
pub mod builder;
|
||||||
pub mod sparse_vector;
|
pub mod sparse_vector;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
|
pub mod weights;
|
||||||
|
|
||||||
|
pub use builder::{ArtistVectorBuilder, BuildResult};
|
||||||
pub use sparse_vector::SparseVector;
|
pub use sparse_vector::SparseVector;
|
||||||
pub use store::{ArtistVectorStore, ArtistVectorStoreState, SimilarArtist, StoreStats, VectorEntry};
|
pub use store::{ArtistVectorStore, ArtistVectorStoreState, SimilarArtist, StoreStats, VectorEntry};
|
||||||
|
pub use weights::RelationshipWeights;
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
//! Configurable weights for different relationship types
|
||||||
|
//!
|
||||||
|
//! These weights determine how strongly different types of relationships
|
||||||
|
//! contribute to artist similarity vectors.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Weights for different relationship types when building artist vectors
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RelationshipWeights {
|
||||||
|
// === MusicBrainz artist-to-artist relationships ===
|
||||||
|
|
||||||
|
/// Weight for band membership (artist was/is member of group)
|
||||||
|
/// Strongest connection - same creative unit
|
||||||
|
pub member_of_band: f32,
|
||||||
|
|
||||||
|
/// Weight for collaboration (artists worked together)
|
||||||
|
pub collaboration: f32,
|
||||||
|
|
||||||
|
/// Weight for "is person" in a group (reverse of member_of_band)
|
||||||
|
pub has_member: f32,
|
||||||
|
|
||||||
|
/// Weight for founder relationship
|
||||||
|
pub founder: f32,
|
||||||
|
|
||||||
|
// === MusicBrainz recording/release relationships ===
|
||||||
|
|
||||||
|
/// Weight for performer credit on a recording
|
||||||
|
pub performer: f32,
|
||||||
|
|
||||||
|
/// Weight for composer credit
|
||||||
|
pub composer: f32,
|
||||||
|
|
||||||
|
/// Weight for producer credit
|
||||||
|
pub producer: f32,
|
||||||
|
|
||||||
|
/// Weight for conductor credit
|
||||||
|
pub conductor: f32,
|
||||||
|
|
||||||
|
/// Weight for engineer/mixer credit
|
||||||
|
pub engineer: f32,
|
||||||
|
|
||||||
|
// === Qobuz relationships ===
|
||||||
|
|
||||||
|
/// Weight for Qobuz similar artists
|
||||||
|
pub qobuz_similar: f32,
|
||||||
|
|
||||||
|
// === Tag-based relationships ===
|
||||||
|
|
||||||
|
/// Weight for shared MusicBrainz tags (genres)
|
||||||
|
pub shared_tag: f32,
|
||||||
|
|
||||||
|
// === Behavioral relationships ===
|
||||||
|
|
||||||
|
/// Weight for user listening affinity (artists played together)
|
||||||
|
pub user_affinity: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RelationshipWeights {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
// Band relationships - strongest
|
||||||
|
member_of_band: 1.0,
|
||||||
|
has_member: 0.9,
|
||||||
|
founder: 0.85,
|
||||||
|
collaboration: 0.8,
|
||||||
|
|
||||||
|
// Credit relationships - medium
|
||||||
|
performer: 0.6,
|
||||||
|
composer: 0.55,
|
||||||
|
producer: 0.5,
|
||||||
|
conductor: 0.5,
|
||||||
|
engineer: 0.3,
|
||||||
|
|
||||||
|
// Qobuz similarity - good signal
|
||||||
|
qobuz_similar: 0.7,
|
||||||
|
|
||||||
|
// Tags - weak but useful
|
||||||
|
shared_tag: 0.3,
|
||||||
|
|
||||||
|
// User behavior - medium
|
||||||
|
user_affinity: 0.5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RelationshipWeights {
|
||||||
|
/// Create weights optimized for discovering band-related artists
|
||||||
|
pub fn band_focused() -> Self {
|
||||||
|
Self {
|
||||||
|
member_of_band: 1.0,
|
||||||
|
has_member: 1.0,
|
||||||
|
founder: 0.9,
|
||||||
|
collaboration: 0.7,
|
||||||
|
performer: 0.4,
|
||||||
|
composer: 0.3,
|
||||||
|
producer: 0.2,
|
||||||
|
conductor: 0.3,
|
||||||
|
engineer: 0.1,
|
||||||
|
qobuz_similar: 0.5,
|
||||||
|
shared_tag: 0.2,
|
||||||
|
user_affinity: 0.3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create weights optimized for sound-alike discovery
|
||||||
|
pub fn similarity_focused() -> Self {
|
||||||
|
Self {
|
||||||
|
member_of_band: 0.6,
|
||||||
|
has_member: 0.5,
|
||||||
|
founder: 0.5,
|
||||||
|
collaboration: 0.7,
|
||||||
|
performer: 0.5,
|
||||||
|
composer: 0.4,
|
||||||
|
producer: 0.3,
|
||||||
|
conductor: 0.3,
|
||||||
|
engineer: 0.2,
|
||||||
|
qobuz_similar: 1.0, // Prioritize Qobuz similarity
|
||||||
|
shared_tag: 0.5,
|
||||||
|
user_affinity: 0.6,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get weight for a MusicBrainz relationship type
|
||||||
|
pub fn weight_for_mb_relation(&self, relation_type: &str) -> f32 {
|
||||||
|
match relation_type.to_lowercase().as_str() {
|
||||||
|
// Band relationships
|
||||||
|
"member of band" | "member_of_band" => self.member_of_band,
|
||||||
|
"has member" | "has_member" => self.has_member,
|
||||||
|
"founder" | "founded" => self.founder,
|
||||||
|
|
||||||
|
// Collaboration
|
||||||
|
"collaboration" | "collaborated" | "collaborator" => self.collaboration,
|
||||||
|
|
||||||
|
// Performance credits
|
||||||
|
"performer" | "vocal" | "instrument" | "performing orchestra"
|
||||||
|
| "orchestra" | "chorus master" => self.performer,
|
||||||
|
|
||||||
|
// Composition
|
||||||
|
"composer" | "writer" | "lyricist" | "librettist" | "arranger" => self.composer,
|
||||||
|
|
||||||
|
// Production
|
||||||
|
"producer" | "executive producer" | "co-producer" => self.producer,
|
||||||
|
"conductor" => self.conductor,
|
||||||
|
"engineer" | "mix" | "mixer" | "mastering" | "recording" => self.engineer,
|
||||||
|
|
||||||
|
// Unknown - use small default weight
|
||||||
|
_ => 0.2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get weight for a source type string
|
||||||
|
pub fn weight_for_source(&self, source: &str) -> f32 {
|
||||||
|
match source {
|
||||||
|
"qobuz_similar" => self.qobuz_similar,
|
||||||
|
"shared_tag" => self.shared_tag,
|
||||||
|
"user_affinity" => self.user_affinity,
|
||||||
|
s if s.starts_with("mb:") => {
|
||||||
|
let rel_type = &s[3..];
|
||||||
|
self.weight_for_mb_relation(rel_type)
|
||||||
|
}
|
||||||
|
_ => 0.2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default_weights() {
|
||||||
|
let weights = RelationshipWeights::default();
|
||||||
|
|
||||||
|
assert_eq!(weights.member_of_band, 1.0);
|
||||||
|
assert!(weights.qobuz_similar > weights.shared_tag);
|
||||||
|
assert!(weights.collaboration > weights.engineer);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mb_relation_weight() {
|
||||||
|
let weights = RelationshipWeights::default();
|
||||||
|
|
||||||
|
assert_eq!(weights.weight_for_mb_relation("member of band"), 1.0);
|
||||||
|
assert_eq!(weights.weight_for_mb_relation("collaboration"), 0.8);
|
||||||
|
assert_eq!(weights.weight_for_mb_relation("producer"), 0.5);
|
||||||
|
|
||||||
|
// Unknown type gets default
|
||||||
|
assert_eq!(weights.weight_for_mb_relation("unknown_type"), 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_source_weight() {
|
||||||
|
let weights = RelationshipWeights::default();
|
||||||
|
|
||||||
|
assert_eq!(weights.weight_for_source("qobuz_similar"), 0.7);
|
||||||
|
assert_eq!(weights.weight_for_source("mb:member of band"), 1.0);
|
||||||
|
assert_eq!(weights.weight_for_source("mb:collaboration"), 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,9 +22,10 @@ pub mod smart_playlists;
|
|||||||
pub use cache::{CacheStats, MusicBrainzCache, MusicBrainzCacheState};
|
pub use cache::{CacheStats, MusicBrainzCache, MusicBrainzCacheState};
|
||||||
pub use client::{MusicBrainzClient, MusicBrainzConfig};
|
pub use client::{MusicBrainzClient, MusicBrainzConfig};
|
||||||
pub use models::{
|
pub use models::{
|
||||||
AlbumAppearance, ArtistRelationships, ArtistType, MatchConfidence, Medium, MediumTrack,
|
AlbumAppearance, ArtistFullResponse, ArtistRelationships, ArtistType, MatchConfidence, Medium,
|
||||||
MusicianAppearances, MusicianConfidence, Period, RelatedArtist, ReleaseFullResponse,
|
MediumTrack, MusicianAppearances, MusicianConfidence, Period, RelatedArtist, Relation,
|
||||||
ReleaseSearchResponse, ResolvedArtist, ResolvedMusician, ResolvedRelease, ResolvedTrack, Tag,
|
ReleaseFullResponse, ReleaseSearchResponse, ResolvedArtist, ResolvedMusician, ResolvedRelease,
|
||||||
|
ResolvedTrack, Tag,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|||||||
Reference in new issue
Block a user