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:
vicrodh committed 2026-04-26 20:26:19 -06:00
1 parent 7d18e6d3a8
commit d98d35796e
2 files changed
+255 -312

No files matched your search

+249 -25
View File
@@ -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
+6 -287
View File
@@ -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>,