feat(radio): expand pool with playlists and similar artists

cx
This commit is contained in:
vicrodh committed 2026-01-20 15:06:39 -06:00
1 parent 029e4a07df
commit b8d78a9003
1 file changed
+136 -4
+136 -4
View File
@@ -8,6 +8,16 @@ pub struct BuildRadioOptions {
pub reseed_every: u32,
pub seed_tracks_limit: u32,
pub playlist_limit: usize,
pub playlist_track_limit: usize,
pub similar_artists_limit: u32,
pub similar_artist_tracks_limit: u32,
pub min_pool_size_for_second_degree: u32,
pub second_degree_artist_limit: u32,
pub second_degree_tracks_limit: u32,
}
impl Default for BuildRadioOptions {
@@ -17,6 +27,16 @@ impl Default for BuildRadioOptions {
artist_spacing: 5,
reseed_every: 25,
seed_tracks_limit: 150,
playlist_limit: 3,
playlist_track_limit: 200,
similar_artists_limit: 12,
similar_artist_tracks_limit: 40,
min_pool_size_for_second_degree: 80,
second_degree_artist_limit: 3,
second_degree_tracks_limit: 30,
}
}
}
@@ -32,6 +52,10 @@ impl<'a> RadioPoolBuilder<'a> {
Self { db, client, options }
}
fn is_qobuz_owned(owner_name: &str) -> bool {
owner_name.to_ascii_lowercase().contains("qobuz")
}
fn track_artist_id(track: &Track, fallback: u64) -> u64 {
if let Some(performer) = track.performer.as_ref() {
if performer.id != 0 {
@@ -65,7 +89,7 @@ impl<'a> RadioPoolBuilder<'a> {
self.options.reseed_every,
)?;
self.add_seed_artist_tracks(&session.id, artist_id).await?;
self.build_pool_for_seed_artist(&session.id, artist_id).await?;
Ok(session)
}
@@ -91,12 +115,50 @@ impl<'a> RadioPoolBuilder<'a> {
.insert_pool_track(&session.id, track.id, track_artist_id, "seed_track", 0)?;
}
self.add_seed_artist_tracks(&session.id, artist_id).await?;
self.build_pool_for_seed_artist(&session.id, artist_id).await?;
Ok(session)
}
async fn add_seed_artist_tracks(&self, session_id: &str, seed_artist_id: u64) -> Result<(), String> {
async fn build_pool_for_seed_artist(&self, session_id: &str, seed_artist_id: u64) -> Result<(), String> {
// 1) Curated artist playlists (distance 1)
let artist_detail = self
.client
.get_artist_detail(seed_artist_id, None, None)
.await
.map_err(|e| format!("Failed to fetch artist detail: {}", e))?;
let mut playlists = artist_detail.playlists.unwrap_or_default();
playlists.retain(|p| Self::is_qobuz_owned(&p.owner.name));
playlists.sort_by(|a, b| {
b.tracks_count
.cmp(&a.tracks_count)
.then_with(|| a.id.cmp(&b.id))
});
for playlist in playlists.into_iter().take(self.options.playlist_limit) {
let playlist = self
.client
.get_playlist(playlist.id)
.await
.map_err(|e| format!("Failed to fetch playlist {}: {}", playlist.id, e))?;
let tracks = playlist
.tracks
.map(|t| t.items)
.unwrap_or_default();
for t in tracks.into_iter().take(self.options.playlist_track_limit) {
if !Self::is_music_track(&t) {
continue;
}
let artist_id = Self::track_artist_id(&t, seed_artist_id);
self.db
.insert_pool_track(session_id, t.id, artist_id, "curated_playlist", 1)?;
}
}
// 2) Seed tracks (distance 0)
let tracks: TracksContainer = self
.client
.get_artist_tracks(seed_artist_id, self.options.seed_tracks_limit, 0)
@@ -112,7 +174,77 @@ impl<'a> RadioPoolBuilder<'a> {
.insert_pool_track(session_id, t.id, artist_id, "seed_tracks", 0)?;
}
// 3) Similar artists (distance 1)
let similar = self
.client
.get_similar_artists(seed_artist_id, self.options.similar_artists_limit, 0)
.await
.map_err(|e| format!("Failed to fetch similar artists: {}", e))?;
let mut first_degree_artist_ids: Vec<u64> = similar.items.into_iter().map(|a| a.id).collect();
first_degree_artist_ids.retain(|id| *id != 0 && *id != seed_artist_id);
first_degree_artist_ids.sort();
first_degree_artist_ids.dedup();
for artist_id in first_degree_artist_ids.iter().copied() {
let tracks = self
.client
.get_artist_tracks(artist_id, self.options.similar_artist_tracks_limit, 0)
.await
.map_err(|e| format!("Failed to fetch similar artist tracks: {}", e))?;
for t in tracks.items {
if !Self::is_music_track(&t) {
continue;
}
let artist_id = Self::track_artist_id(&t, artist_id);
self.db
.insert_pool_track(session_id, t.id, artist_id, "similar_artist", 1)?;
}
}
// 4) Second-degree expansion (distance 2, bounded)
let pool_size = self.db.pool_size(session_id)?;
if pool_size < self.options.min_pool_size_for_second_degree && self.options.second_degree_artist_limit > 0 {
let mut added = 0u32;
for base_artist_id in first_degree_artist_ids.into_iter().take(2) {
if added >= self.options.second_degree_artist_limit {
break;
}
let second = self
.client
.get_similar_artists(base_artist_id, self.options.second_degree_artist_limit, 0)
.await
.map_err(|e| format!("Failed to fetch second-degree similar artists: {}", e))?;
for a in second.items {
if added >= self.options.second_degree_artist_limit {
break;
}
if a.id == 0 || a.id == seed_artist_id || a.id == base_artist_id {
continue;
}
let tracks = self
.client
.get_artist_tracks(a.id, self.options.second_degree_tracks_limit, 0)
.await
.map_err(|e| format!("Failed to fetch second-degree artist tracks: {}", e))?;
for t in tracks.items {
if !Self::is_music_track(&t) {
continue;
}
let artist_id = Self::track_artist_id(&t, a.id);
self.db
.insert_pool_track(session_id, t.id, artist_id, "second_degree", 2)?;
}
added += 1;
}
}
}
Ok(())
}
}