feat(radio): expand pool with playlists and similar artists
cx
This commit is contained in:
1 parent
029e4a07df
commit
b8d78a9003
1 file changed
+136
-4
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user