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>,
|
limitFavorites: Option<u32>,
|
||||||
state: State<'_, RecoState>,
|
state: State<'_, RecoState>,
|
||||||
) -> Result<HomeSeeds, String> {
|
) -> Result<HomeSeeds, String> {
|
||||||
crate::reco_store::commands::reco_get_home(
|
let limit_recent_albums = limitRecentAlbums.unwrap_or(12);
|
||||||
limitRecentAlbums,
|
let limit_continue_tracks = limitContinueTracks.unwrap_or(10);
|
||||||
limitContinueTracks,
|
let limit_top_artists = limitTopArtists.unwrap_or(10);
|
||||||
limitTopArtists,
|
let limit_favorites = limitFavorites.unwrap_or(12);
|
||||||
limitFavorites,
|
|
||||||
state,
|
let guard__ = state.db.lock().await;
|
||||||
)
|
let db = guard__
|
||||||
.await
|
.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]
|
#[tauri::command]
|
||||||
@@ -1695,14 +1710,23 @@ pub async fn v2_reco_get_home_ml(
|
|||||||
limitFavorites: Option<u32>,
|
limitFavorites: Option<u32>,
|
||||||
state: State<'_, RecoState>,
|
state: State<'_, RecoState>,
|
||||||
) -> Result<HomeSeeds, String> {
|
) -> Result<HomeSeeds, String> {
|
||||||
crate::reco_store::commands::reco_get_home_ml(
|
let limit_recent_albums = limitRecentAlbums.unwrap_or(12);
|
||||||
limitRecentAlbums,
|
let limit_continue_tracks = limitContinueTracks.unwrap_or(10);
|
||||||
limitContinueTracks,
|
let limit_top_artists = limitTopArtists.unwrap_or(10);
|
||||||
limitTopArtists,
|
let limit_favorites = limitFavorites.unwrap_or(12);
|
||||||
limitFavorites,
|
|
||||||
state,
|
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]
|
#[tauri::command]
|
||||||
@@ -1716,16 +1740,216 @@ pub async fn v2_reco_get_home_resolved(
|
|||||||
app_state: State<'_, AppState>,
|
app_state: State<'_, AppState>,
|
||||||
cache_state: State<'_, ApiCacheState>,
|
cache_state: State<'_, ApiCacheState>,
|
||||||
) -> Result<HomeResolved, String> {
|
) -> Result<HomeResolved, String> {
|
||||||
crate::reco_store::commands::reco_get_home_resolved(
|
use rand::seq::SliceRandom;
|
||||||
limitRecentAlbums,
|
use std::collections::HashMap;
|
||||||
limitContinueTracks,
|
|
||||||
limitTopArtists,
|
let limit_recent_albums = limitRecentAlbums.unwrap_or(12);
|
||||||
limitFavorites,
|
let limit_continue_tracks = limitContinueTracks.unwrap_or(10);
|
||||||
reco_state,
|
let limit_top_artists = limitTopArtists.unwrap_or(10);
|
||||||
app_state,
|
let limit_favorites = limitFavorites.unwrap_or(12);
|
||||||
cache_state,
|
|
||||||
)
|
// Step 1: Get seeds (IDs) from reco DB
|
||||||
.await
|
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
|
/// Get album suggestions (similar albums) from Qobuz /album/suggest API
|
||||||
|
|||||||
@@ -3,16 +3,14 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use rand::seq::SliceRandom;
|
|
||||||
|
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
use crate::api::models::{Album, Artist, ImageSet, Track};
|
use crate::api::models::{Album, Artist, ImageSet, Track};
|
||||||
use crate::api_cache::ApiCacheState;
|
use crate::api_cache::ApiCacheState;
|
||||||
use crate::reco_store::db::{RecoEventRecord, RecoScoreEntry};
|
use crate::reco_store::db::{RecoEventRecord, RecoScoreEntry};
|
||||||
use crate::reco_store::{
|
use crate::reco_store::{
|
||||||
AlbumCardMeta, ArtistCardMeta, HomeResolved, HomeSeeds, RecoEventInput, RecoState,
|
AlbumCardMeta, ArtistCardMeta, HomeSeeds, RecoEventInput, RecoState, TopArtistSeed,
|
||||||
TopArtistSeed, TrackDisplayMeta,
|
TrackDisplayMeta,
|
||||||
};
|
};
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
@@ -56,39 +54,6 @@ pub async fn reco_log_event(
|
|||||||
db.insert_event(&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]
|
#[tauri::command]
|
||||||
pub async fn reco_train_scores(
|
pub async fn reco_train_scores(
|
||||||
lookback_days: Option<i64>,
|
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)
|
/// Merge two lists preserving order: fresh items first, then scored items (excluding duplicates)
|
||||||
fn merge_unique_preserve_order<T: Eq + std::hash::Hash + Clone>(
|
fn merge_unique_preserve_order<T: Eq + std::hash::Hash + Clone>(
|
||||||
fresh: Vec<T>,
|
fresh: Vec<T>,
|
||||||
@@ -590,7 +528,7 @@ fn get_image(image: &ImageSet) -> String {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn album_to_card_meta(album: &Album) -> AlbumCardMeta {
|
pub(crate) fn album_to_card_meta(album: &Album) -> AlbumCardMeta {
|
||||||
AlbumCardMeta {
|
AlbumCardMeta {
|
||||||
id: album.id.clone(),
|
id: album.id.clone(),
|
||||||
artwork: get_image(&album.image),
|
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
|
/// 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,
|
db: &crate::reco_store::db::RecoStoreDb,
|
||||||
limit_recent_albums: u32,
|
limit_recent_albums: u32,
|
||||||
limit_continue_tracks: 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
|
/// Resolve album IDs → AlbumCardMeta with 3-tier cache
|
||||||
pub async fn resolve_albums(
|
pub async fn resolve_albums(
|
||||||
@@ -1111,7 +830,7 @@ pub async fn resolve_albums(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve track IDs → TrackDisplayMeta with 3-tier cache
|
/// Resolve track IDs → TrackDisplayMeta with 3-tier cache
|
||||||
async fn resolve_tracks(
|
pub(crate) async fn resolve_tracks(
|
||||||
ids: &[u64],
|
ids: &[u64],
|
||||||
reco_state: &State<'_, RecoState>,
|
reco_state: &State<'_, RecoState>,
|
||||||
app_state: &State<'_, AppState>,
|
app_state: &State<'_, AppState>,
|
||||||
@@ -1249,7 +968,7 @@ async fn resolve_tracks(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve artist IDs → ArtistCardMeta with 3-tier cache
|
/// Resolve artist IDs → ArtistCardMeta with 3-tier cache
|
||||||
async fn resolve_artists(
|
pub(crate) async fn resolve_artists(
|
||||||
ids: &[u64],
|
ids: &[u64],
|
||||||
play_counts: &HashMap<u64, u32>,
|
play_counts: &HashMap<u64, u32>,
|
||||||
reco_state: &State<'_, RecoState>,
|
reco_state: &State<'_, RecoState>,
|
||||||
|
|||||||
Reference in new issue
Block a user