refactor(v2): inline reco_get_home, reco_get_home_ml, reco_get_home_resolved
Inlines bodies of reco_get_home, reco_get_home_ml, and reco_get_home_resolved from reco_store/commands.rs directly into the v2 wrappers in commands_v2/library.rs. Promotes their shared helpers (get_home_seeds_internal, resolve_tracks, resolve_artists, album_to_card_meta) from private to pub(crate) so the v2 wrappers can call them without crossing back into a legacy command module. Deletes the corresponding legacy #[tauri::command] definitions and prunes now-unused imports from reco_store/commands.rs. Step 8 of legacy cleanup, group G. cc
This commit is contained in:
2 files changed
+255
-312
No files matched your search
@@ -1676,14 +1676,29 @@ pub async fn v2_reco_get_home(
|
||||
limitFavorites: Option<u32>,
|
||||
state: State<'_, RecoState>,
|
||||
) -> Result<HomeSeeds, String> {
|
||||
crate::reco_store::commands::reco_get_home(
|
||||
limitRecentAlbums,
|
||||
limitContinueTracks,
|
||||
limitTopArtists,
|
||||
limitFavorites,
|
||||
state,
|
||||
)
|
||||
.await
|
||||
let limit_recent_albums = limitRecentAlbums.unwrap_or(12);
|
||||
let limit_continue_tracks = limitContinueTracks.unwrap_or(10);
|
||||
let limit_top_artists = limitTopArtists.unwrap_or(10);
|
||||
let limit_favorites = limitFavorites.unwrap_or(12);
|
||||
|
||||
let guard__ = state.db.lock().await;
|
||||
let db = guard__
|
||||
.as_ref()
|
||||
.ok_or("No active session - please log in")?;
|
||||
|
||||
let recently_played_album_ids = db.get_recent_album_ids(limit_recent_albums)?;
|
||||
let continue_listening_track_ids = db.get_recent_track_ids(limit_continue_tracks)?;
|
||||
let top_artist_ids = db.get_top_artist_ids(limit_top_artists)?;
|
||||
let favorite_album_ids = db.get_favorite_album_ids(limit_favorites)?;
|
||||
let favorite_track_ids = db.get_favorite_track_ids(limit_favorites)?;
|
||||
|
||||
Ok(HomeSeeds {
|
||||
recently_played_album_ids,
|
||||
continue_listening_track_ids,
|
||||
top_artist_ids,
|
||||
favorite_album_ids,
|
||||
favorite_track_ids,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1695,14 +1710,23 @@ pub async fn v2_reco_get_home_ml(
|
||||
limitFavorites: Option<u32>,
|
||||
state: State<'_, RecoState>,
|
||||
) -> Result<HomeSeeds, String> {
|
||||
crate::reco_store::commands::reco_get_home_ml(
|
||||
limitRecentAlbums,
|
||||
limitContinueTracks,
|
||||
limitTopArtists,
|
||||
limitFavorites,
|
||||
state,
|
||||
let limit_recent_albums = limitRecentAlbums.unwrap_or(12);
|
||||
let limit_continue_tracks = limitContinueTracks.unwrap_or(10);
|
||||
let limit_top_artists = limitTopArtists.unwrap_or(10);
|
||||
let limit_favorites = limitFavorites.unwrap_or(12);
|
||||
|
||||
let guard__ = state.db.lock().await;
|
||||
let db = guard__
|
||||
.as_ref()
|
||||
.ok_or("No active session - please log in")?;
|
||||
|
||||
crate::reco_store::commands::get_home_seeds_internal(
|
||||
db,
|
||||
limit_recent_albums,
|
||||
limit_continue_tracks,
|
||||
limit_top_artists,
|
||||
limit_favorites,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1716,16 +1740,216 @@ pub async fn v2_reco_get_home_resolved(
|
||||
app_state: State<'_, AppState>,
|
||||
cache_state: State<'_, ApiCacheState>,
|
||||
) -> Result<HomeResolved, String> {
|
||||
crate::reco_store::commands::reco_get_home_resolved(
|
||||
limitRecentAlbums,
|
||||
limitContinueTracks,
|
||||
limitTopArtists,
|
||||
limitFavorites,
|
||||
reco_state,
|
||||
app_state,
|
||||
cache_state,
|
||||
)
|
||||
.await
|
||||
use rand::seq::SliceRandom;
|
||||
use std::collections::HashMap;
|
||||
|
||||
let limit_recent_albums = limitRecentAlbums.unwrap_or(12);
|
||||
let limit_continue_tracks = limitContinueTracks.unwrap_or(10);
|
||||
let limit_top_artists = limitTopArtists.unwrap_or(10);
|
||||
let limit_favorites = limitFavorites.unwrap_or(12);
|
||||
|
||||
// Step 1: Get seeds (IDs) from reco DB
|
||||
let seeds = {
|
||||
let guard__ = reco_state.db.lock().await;
|
||||
let db = guard__
|
||||
.as_ref()
|
||||
.ok_or("No active session - please log in")?;
|
||||
crate::reco_store::commands::get_home_seeds_internal(
|
||||
db,
|
||||
limit_recent_albums,
|
||||
limit_continue_tracks,
|
||||
limit_top_artists,
|
||||
limit_favorites,
|
||||
)?
|
||||
};
|
||||
|
||||
// Step 2: Collect all unique IDs needed
|
||||
// For favorite albums: merge favorite album IDs + album IDs from favorite tracks
|
||||
// (favorite tracks → their albums is done after track resolution)
|
||||
let all_track_ids: Vec<u64> = {
|
||||
let mut ids = seeds.continue_listening_track_ids.clone();
|
||||
ids.extend(&seeds.favorite_track_ids);
|
||||
ids.sort_unstable();
|
||||
ids.dedup();
|
||||
ids
|
||||
};
|
||||
|
||||
let all_artist_ids: Vec<u64> = seeds.top_artist_ids.iter().map(|s| s.artist_id).collect();
|
||||
|
||||
// Build play_count map for artists
|
||||
let artist_play_counts: HashMap<u64, u32> = seeds
|
||||
.top_artist_ids
|
||||
.iter()
|
||||
.map(|s| (s.artist_id, s.play_count))
|
||||
.collect();
|
||||
|
||||
// Step 3: Resolve all entity types in parallel.
|
||||
// Recent albums, favorite albums, tracks, and artists are all independent.
|
||||
let recent_albums_fut = crate::reco_store::commands::resolve_albums(
|
||||
&seeds.recently_played_album_ids,
|
||||
&reco_state,
|
||||
&app_state,
|
||||
&cache_state,
|
||||
);
|
||||
let favorite_albums_fut = crate::reco_store::commands::resolve_albums(
|
||||
&seeds.favorite_album_ids,
|
||||
&reco_state,
|
||||
&app_state,
|
||||
&cache_state,
|
||||
);
|
||||
let tracks_fut = crate::reco_store::commands::resolve_tracks(
|
||||
&all_track_ids,
|
||||
&reco_state,
|
||||
&app_state,
|
||||
&cache_state,
|
||||
);
|
||||
let artists_fut = crate::reco_store::commands::resolve_artists(
|
||||
&all_artist_ids,
|
||||
&artist_play_counts,
|
||||
&reco_state,
|
||||
&app_state,
|
||||
&cache_state,
|
||||
);
|
||||
|
||||
let (recently_played_albums, resolved_favorites, all_tracks, top_artists) = tokio::try_join!(
|
||||
recent_albums_fut,
|
||||
favorite_albums_fut,
|
||||
tracks_fut,
|
||||
artists_fut
|
||||
)?;
|
||||
|
||||
// Build track lookup
|
||||
let track_map: HashMap<u64, &crate::reco_store::TrackDisplayMeta> =
|
||||
all_tracks.iter().map(|tr| (tr.id, tr)).collect();
|
||||
|
||||
let continue_listening_tracks: Vec<crate::reco_store::TrackDisplayMeta> = seeds
|
||||
.continue_listening_track_ids
|
||||
.iter()
|
||||
.filter_map(|id| track_map.get(id).map(|tr| (*tr).clone()))
|
||||
.collect();
|
||||
|
||||
// Step 4: "More From Favorites" = discover albums by favorite artists,
|
||||
// excluding albums the user already has in favorites / recently played.
|
||||
|
||||
// 5b: Collect unique artist IDs from favorite albums + favorite tracks
|
||||
let favorite_artist_ids: Vec<u64> = {
|
||||
let mut ids = Vec::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for album in &resolved_favorites {
|
||||
if let Some(aid) = album.artist_id {
|
||||
if seen.insert(aid) {
|
||||
ids.push(aid);
|
||||
}
|
||||
}
|
||||
}
|
||||
for track_id in &seeds.favorite_track_ids {
|
||||
if let Some(track) = track_map.get(track_id) {
|
||||
if let Some(aid) = track.artist_id {
|
||||
if seen.insert(aid) {
|
||||
ids.push(aid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ids
|
||||
};
|
||||
|
||||
// 5c: Build exclusion set (already-favorited + recently played albums)
|
||||
let exclusion_set: std::sync::Arc<std::collections::HashSet<String>> = {
|
||||
let mut set: std::collections::HashSet<String> =
|
||||
seeds.favorite_album_ids.iter().cloned().collect();
|
||||
for track_id in &seeds.favorite_track_ids {
|
||||
if let Some(track) = track_map.get(track_id) {
|
||||
if let Some(ref album_id) = track.album_id {
|
||||
if !album_id.is_empty() {
|
||||
set.insert(album_id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for album_id in &seeds.recently_played_album_ids {
|
||||
set.insert(album_id.clone());
|
||||
}
|
||||
std::sync::Arc::new(set)
|
||||
};
|
||||
|
||||
// 5d: Fetch albums from favorite artists (parallel)
|
||||
// Shuffle artists so every favorite gets a fair chance, cap 2 albums per artist
|
||||
const MAX_ALBUMS_PER_ARTIST: usize = 2;
|
||||
let favorite_albums = if favorite_artist_ids.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
// Shuffle artists so every favorite gets a fair chance
|
||||
let shuffled_artists = {
|
||||
let mut ids = favorite_artist_ids.clone();
|
||||
ids.shuffle(&mut rand::rng());
|
||||
ids
|
||||
};
|
||||
|
||||
let sem = std::sync::Arc::new(tokio::sync::Semaphore::new(5));
|
||||
let client = app_state.client.clone();
|
||||
let reco_arc = reco_state.db.clone();
|
||||
|
||||
let mut handles = Vec::new();
|
||||
for artist_id in shuffled_artists.iter().take(14) {
|
||||
let sem = sem.clone();
|
||||
let client = client.clone();
|
||||
let reco_arc = reco_arc.clone();
|
||||
let exclusion = exclusion_set.clone();
|
||||
let artist_id = *artist_id;
|
||||
|
||||
handles.push(tokio::spawn(async move {
|
||||
let _permit = sem.acquire().await.ok()?;
|
||||
let artist = {
|
||||
let c = client.read().await;
|
||||
c.get_artist_with_pagination_and_locale(artist_id, true, Some(25), None, None)
|
||||
.await
|
||||
.ok()?
|
||||
};
|
||||
let albums = artist.albums?;
|
||||
let mut results: Vec<crate::reco_store::AlbumCardMeta> = Vec::new();
|
||||
for album in &albums.items {
|
||||
if results.len() >= MAX_ALBUMS_PER_ARTIST {
|
||||
break;
|
||||
}
|
||||
if !exclusion.contains(&album.id) {
|
||||
let meta = crate::reco_store::commands::album_to_card_meta(album);
|
||||
{
|
||||
let guard__ = reco_arc.lock().await;
|
||||
if let Some(db) = guard__.as_ref() {
|
||||
let _ = db.set_album_meta(&meta);
|
||||
}
|
||||
}
|
||||
results.push(meta);
|
||||
}
|
||||
}
|
||||
Some(results)
|
||||
}));
|
||||
}
|
||||
|
||||
let mut all_albums: Vec<crate::reco_store::AlbumCardMeta> = Vec::new();
|
||||
for handle in handles {
|
||||
if let Ok(Some(albums)) = handle.await {
|
||||
all_albums.extend(albums);
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate, shuffle, and limit
|
||||
{
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
all_albums.retain(|a| seen.insert(a.id.clone()));
|
||||
}
|
||||
all_albums.shuffle(&mut rand::rng());
|
||||
all_albums.truncate(limit_favorites as usize);
|
||||
all_albums
|
||||
};
|
||||
|
||||
Ok(HomeResolved {
|
||||
recently_played_albums,
|
||||
continue_listening_tracks,
|
||||
top_artists,
|
||||
favorite_albums,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get album suggestions (similar albums) from Qobuz /album/suggest API
|
||||
|
||||
@@ -3,16 +3,14 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use rand::seq::SliceRandom;
|
||||
|
||||
use tauri::State;
|
||||
|
||||
use crate::api::models::{Album, Artist, ImageSet, Track};
|
||||
use crate::api_cache::ApiCacheState;
|
||||
use crate::reco_store::db::{RecoEventRecord, RecoScoreEntry};
|
||||
use crate::reco_store::{
|
||||
AlbumCardMeta, ArtistCardMeta, HomeResolved, HomeSeeds, RecoEventInput, RecoState,
|
||||
TopArtistSeed, TrackDisplayMeta,
|
||||
AlbumCardMeta, ArtistCardMeta, HomeSeeds, RecoEventInput, RecoState, TopArtistSeed,
|
||||
TrackDisplayMeta,
|
||||
};
|
||||
use crate::AppState;
|
||||
|
||||
@@ -56,39 +54,6 @@ pub async fn reco_log_event(
|
||||
db.insert_event(&event)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn reco_get_home(
|
||||
limit_recent_albums: Option<u32>,
|
||||
limit_continue_tracks: Option<u32>,
|
||||
limit_top_artists: Option<u32>,
|
||||
limit_favorites: Option<u32>,
|
||||
state: State<'_, RecoState>,
|
||||
) -> Result<HomeSeeds, String> {
|
||||
let limit_recent_albums = limit_recent_albums.unwrap_or(12);
|
||||
let limit_continue_tracks = limit_continue_tracks.unwrap_or(10);
|
||||
let limit_top_artists = limit_top_artists.unwrap_or(10);
|
||||
let limit_favorites = limit_favorites.unwrap_or(12);
|
||||
|
||||
let guard__ = state.db.lock().await;
|
||||
let db = guard__
|
||||
.as_ref()
|
||||
.ok_or("No active session - please log in")?;
|
||||
|
||||
let recently_played_album_ids = db.get_recent_album_ids(limit_recent_albums)?;
|
||||
let continue_listening_track_ids = db.get_recent_track_ids(limit_continue_tracks)?;
|
||||
let top_artist_ids = db.get_top_artist_ids(limit_top_artists)?;
|
||||
let favorite_album_ids = db.get_favorite_album_ids(limit_favorites)?;
|
||||
let favorite_track_ids = db.get_favorite_track_ids(limit_favorites)?;
|
||||
|
||||
Ok(HomeSeeds {
|
||||
recently_played_album_ids,
|
||||
continue_listening_track_ids,
|
||||
top_artist_ids,
|
||||
favorite_album_ids,
|
||||
favorite_track_ids,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn reco_train_scores(
|
||||
lookback_days: Option<i64>,
|
||||
@@ -151,33 +116,6 @@ pub async fn reco_train_scores(
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn reco_get_home_ml(
|
||||
limit_recent_albums: Option<u32>,
|
||||
limit_continue_tracks: Option<u32>,
|
||||
limit_top_artists: Option<u32>,
|
||||
limit_favorites: Option<u32>,
|
||||
state: State<'_, RecoState>,
|
||||
) -> Result<HomeSeeds, String> {
|
||||
let limit_recent_albums = limit_recent_albums.unwrap_or(12);
|
||||
let limit_continue_tracks = limit_continue_tracks.unwrap_or(10);
|
||||
let limit_top_artists = limit_top_artists.unwrap_or(10);
|
||||
let limit_favorites = limit_favorites.unwrap_or(12);
|
||||
|
||||
let guard__ = state.db.lock().await;
|
||||
let db = guard__
|
||||
.as_ref()
|
||||
.ok_or("No active session - please log in")?;
|
||||
|
||||
get_home_seeds_internal(
|
||||
db,
|
||||
limit_recent_albums,
|
||||
limit_continue_tracks,
|
||||
limit_top_artists,
|
||||
limit_favorites,
|
||||
)
|
||||
}
|
||||
|
||||
/// Merge two lists preserving order: fresh items first, then scored items (excluding duplicates)
|
||||
fn merge_unique_preserve_order<T: Eq + std::hash::Hash + Clone>(
|
||||
fresh: Vec<T>,
|
||||
@@ -590,7 +528,7 @@ fn get_image(image: &ImageSet) -> String {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn album_to_card_meta(album: &Album) -> AlbumCardMeta {
|
||||
pub(crate) fn album_to_card_meta(album: &Album) -> AlbumCardMeta {
|
||||
AlbumCardMeta {
|
||||
id: album.id.clone(),
|
||||
artwork: get_image(&album.image),
|
||||
@@ -662,7 +600,7 @@ fn artist_to_card_meta(artist: &Artist, play_count: Option<u32>) -> ArtistCardMe
|
||||
}
|
||||
|
||||
/// Internal seed-gathering logic shared between reco_get_home_ml and reco_get_home_resolved
|
||||
fn get_home_seeds_internal(
|
||||
pub(crate) fn get_home_seeds_internal(
|
||||
db: &crate::reco_store::db::RecoStoreDb,
|
||||
limit_recent_albums: u32,
|
||||
limit_continue_tracks: u32,
|
||||
@@ -751,225 +689,6 @@ fn get_home_seeds_internal(
|
||||
})
|
||||
}
|
||||
|
||||
/// Return fully-resolved home page data in a single IPC call.
|
||||
///
|
||||
/// 3-tier resolution per entity type:
|
||||
/// 1. Reco meta (SQLite, no TTL) — sub-ms, covers previously played items
|
||||
/// 2. API cache (SQLite, 24h TTL) — sub-ms, covers recent API fetches
|
||||
/// 3. Qobuz API (HTTP, parallel) — fallback for truly new items
|
||||
#[tauri::command]
|
||||
pub async fn reco_get_home_resolved(
|
||||
limit_recent_albums: Option<u32>,
|
||||
limit_continue_tracks: Option<u32>,
|
||||
limit_top_artists: Option<u32>,
|
||||
limit_favorites: Option<u32>,
|
||||
reco_state: State<'_, RecoState>,
|
||||
app_state: State<'_, AppState>,
|
||||
cache_state: State<'_, ApiCacheState>,
|
||||
) -> Result<HomeResolved, String> {
|
||||
let limit_recent_albums = limit_recent_albums.unwrap_or(12);
|
||||
let limit_continue_tracks = limit_continue_tracks.unwrap_or(10);
|
||||
let limit_top_artists = limit_top_artists.unwrap_or(10);
|
||||
let limit_favorites = limit_favorites.unwrap_or(12);
|
||||
|
||||
// Step 1: Get seeds (IDs) from reco DB
|
||||
let seeds = {
|
||||
let guard__ = reco_state.db.lock().await;
|
||||
let db = guard__
|
||||
.as_ref()
|
||||
.ok_or("No active session - please log in")?;
|
||||
get_home_seeds_internal(
|
||||
db,
|
||||
limit_recent_albums,
|
||||
limit_continue_tracks,
|
||||
limit_top_artists,
|
||||
limit_favorites,
|
||||
)?
|
||||
};
|
||||
|
||||
// Step 2: Collect all unique IDs needed
|
||||
// For favorite albums: merge favorite album IDs + album IDs from favorite tracks
|
||||
// (favorite tracks → their albums is done after track resolution)
|
||||
let all_track_ids: Vec<u64> = {
|
||||
let mut ids = seeds.continue_listening_track_ids.clone();
|
||||
ids.extend(&seeds.favorite_track_ids);
|
||||
ids.sort_unstable();
|
||||
ids.dedup();
|
||||
ids
|
||||
};
|
||||
|
||||
let all_artist_ids: Vec<u64> = seeds.top_artist_ids.iter().map(|s| s.artist_id).collect();
|
||||
|
||||
// Build play_count map for artists
|
||||
let artist_play_counts: HashMap<u64, u32> = seeds
|
||||
.top_artist_ids
|
||||
.iter()
|
||||
.map(|s| (s.artist_id, s.play_count))
|
||||
.collect();
|
||||
|
||||
// Step 3: Resolve all entity types in parallel.
|
||||
// Recent albums, favorite albums, tracks, and artists are all independent.
|
||||
let recent_albums_fut = resolve_albums(
|
||||
&seeds.recently_played_album_ids,
|
||||
&reco_state,
|
||||
&app_state,
|
||||
&cache_state,
|
||||
);
|
||||
let favorite_albums_fut = resolve_albums(
|
||||
&seeds.favorite_album_ids,
|
||||
&reco_state,
|
||||
&app_state,
|
||||
&cache_state,
|
||||
);
|
||||
let tracks_fut = resolve_tracks(&all_track_ids, &reco_state, &app_state, &cache_state);
|
||||
let artists_fut = resolve_artists(
|
||||
&all_artist_ids,
|
||||
&artist_play_counts,
|
||||
&reco_state,
|
||||
&app_state,
|
||||
&cache_state,
|
||||
);
|
||||
|
||||
let (recently_played_albums, resolved_favorites, all_tracks, top_artists) = tokio::try_join!(
|
||||
recent_albums_fut,
|
||||
favorite_albums_fut,
|
||||
tracks_fut,
|
||||
artists_fut
|
||||
)?;
|
||||
|
||||
// Build track lookup
|
||||
let track_map: HashMap<u64, &TrackDisplayMeta> =
|
||||
all_tracks.iter().map(|tr| (tr.id, tr)).collect();
|
||||
|
||||
let continue_listening_tracks: Vec<TrackDisplayMeta> = seeds
|
||||
.continue_listening_track_ids
|
||||
.iter()
|
||||
.filter_map(|id| track_map.get(id).map(|tr| (*tr).clone()))
|
||||
.collect();
|
||||
|
||||
// Step 4: "More From Favorites" = discover albums by favorite artists,
|
||||
// excluding albums the user already has in favorites / recently played.
|
||||
|
||||
// 5b: Collect unique artist IDs from favorite albums + favorite tracks
|
||||
let favorite_artist_ids: Vec<u64> = {
|
||||
let mut ids = Vec::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for album in &resolved_favorites {
|
||||
if let Some(aid) = album.artist_id {
|
||||
if seen.insert(aid) {
|
||||
ids.push(aid);
|
||||
}
|
||||
}
|
||||
}
|
||||
for track_id in &seeds.favorite_track_ids {
|
||||
if let Some(track) = track_map.get(track_id) {
|
||||
if let Some(aid) = track.artist_id {
|
||||
if seen.insert(aid) {
|
||||
ids.push(aid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ids
|
||||
};
|
||||
|
||||
// 5c: Build exclusion set (already-favorited + recently played albums)
|
||||
let exclusion_set: std::sync::Arc<std::collections::HashSet<String>> = {
|
||||
let mut set: std::collections::HashSet<String> =
|
||||
seeds.favorite_album_ids.iter().cloned().collect();
|
||||
for track_id in &seeds.favorite_track_ids {
|
||||
if let Some(track) = track_map.get(track_id) {
|
||||
if let Some(ref album_id) = track.album_id {
|
||||
if !album_id.is_empty() {
|
||||
set.insert(album_id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for album_id in &seeds.recently_played_album_ids {
|
||||
set.insert(album_id.clone());
|
||||
}
|
||||
std::sync::Arc::new(set)
|
||||
};
|
||||
|
||||
// 5d: Fetch albums from favorite artists (parallel)
|
||||
// Shuffle artists so every favorite gets a fair chance, cap 2 albums per artist
|
||||
const MAX_ALBUMS_PER_ARTIST: usize = 2;
|
||||
let favorite_albums = if favorite_artist_ids.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
// Shuffle artists so every favorite gets a fair chance
|
||||
let shuffled_artists = {
|
||||
let mut ids = favorite_artist_ids.clone();
|
||||
ids.shuffle(&mut rand::rng());
|
||||
ids
|
||||
};
|
||||
|
||||
let sem = std::sync::Arc::new(tokio::sync::Semaphore::new(5));
|
||||
let client = app_state.client.clone();
|
||||
let reco_arc = reco_state.db.clone();
|
||||
|
||||
let mut handles = Vec::new();
|
||||
for artist_id in shuffled_artists.iter().take(14) {
|
||||
let sem = sem.clone();
|
||||
let client = client.clone();
|
||||
let reco_arc = reco_arc.clone();
|
||||
let exclusion = exclusion_set.clone();
|
||||
let artist_id = *artist_id;
|
||||
|
||||
handles.push(tokio::spawn(async move {
|
||||
let _permit = sem.acquire().await.ok()?;
|
||||
let artist = {
|
||||
let c = client.read().await;
|
||||
c.get_artist_with_pagination_and_locale(artist_id, true, Some(25), None, None)
|
||||
.await
|
||||
.ok()?
|
||||
};
|
||||
let albums = artist.albums?;
|
||||
let mut results: Vec<AlbumCardMeta> = Vec::new();
|
||||
for album in &albums.items {
|
||||
if results.len() >= MAX_ALBUMS_PER_ARTIST {
|
||||
break;
|
||||
}
|
||||
if !exclusion.contains(&album.id) {
|
||||
let meta = album_to_card_meta(album);
|
||||
{
|
||||
let guard__ = reco_arc.lock().await;
|
||||
if let Some(db) = guard__.as_ref() {
|
||||
let _ = db.set_album_meta(&meta);
|
||||
}
|
||||
}
|
||||
results.push(meta);
|
||||
}
|
||||
}
|
||||
Some(results)
|
||||
}));
|
||||
}
|
||||
|
||||
let mut all_albums: Vec<AlbumCardMeta> = Vec::new();
|
||||
for handle in handles {
|
||||
if let Ok(Some(albums)) = handle.await {
|
||||
all_albums.extend(albums);
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate, shuffle, and limit
|
||||
{
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
all_albums.retain(|a| seen.insert(a.id.clone()));
|
||||
}
|
||||
all_albums.shuffle(&mut rand::rng());
|
||||
all_albums.truncate(limit_favorites as usize);
|
||||
all_albums
|
||||
};
|
||||
|
||||
Ok(HomeResolved {
|
||||
recently_played_albums,
|
||||
continue_listening_tracks,
|
||||
top_artists,
|
||||
favorite_albums,
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolve album IDs → AlbumCardMeta with 3-tier cache
|
||||
pub async fn resolve_albums(
|
||||
@@ -1111,7 +830,7 @@ pub async fn resolve_albums(
|
||||
}
|
||||
|
||||
/// Resolve track IDs → TrackDisplayMeta with 3-tier cache
|
||||
async fn resolve_tracks(
|
||||
pub(crate) async fn resolve_tracks(
|
||||
ids: &[u64],
|
||||
reco_state: &State<'_, RecoState>,
|
||||
app_state: &State<'_, AppState>,
|
||||
@@ -1249,7 +968,7 @@ async fn resolve_tracks(
|
||||
}
|
||||
|
||||
/// Resolve artist IDs → ArtistCardMeta with 3-tier cache
|
||||
async fn resolve_artists(
|
||||
pub(crate) async fn resolve_artists(
|
||||
ids: &[u64],
|
||||
play_counts: &HashMap<u64, u32>,
|
||||
reco_state: &State<'_, RecoState>,
|
||||
|
||||
Reference in new issue
Block a user