diff --git a/src-tauri/src/commands_v2.rs b/src-tauri/src/commands_v2.rs index 8e21212e1..f48b0d1af 100644 --- a/src-tauri/src/commands_v2.rs +++ b/src-tauri/src/commands_v2.rs @@ -18,6 +18,7 @@ use qbz_models::{ use crate::artist_blacklist::BlacklistState; use crate::artist_vectors::ArtistVectorStoreState; use crate::cache::{AudioCache, CacheStats}; +use crate::api::models::PlaylistWithTrackIds; use crate::api_cache::ApiCacheState; use crate::audio::{AlsaPlugin, AudioBackendType, AudioDevice, BackendManager}; use crate::cast::{AirPlayMetadata, AirPlayState, CastState, DlnaMetadata, DlnaState, MediaMetadata}; @@ -32,7 +33,7 @@ use crate::config::tray_settings::TraySettings; use crate::config::tray_settings::TraySettingsState; use crate::config::window_settings::WindowSettingsState; use crate::core_bridge::CoreBridgeState; -use crate::library::{thumbnails, LibraryState, LocalAlbum, MetadataExtractor, PlaylistLocalTrack, PlaylistSettings, PlaylistStats, get_artwork_cache_dir}; +use crate::library::{thumbnails, LibraryState, LocalAlbum, LocalTrack, MetadataExtractor, PlaylistLocalTrack, PlaylistSettings, PlaylistStats, ScanProgress, get_artwork_cache_dir}; use crate::lyrics::LyricsState; use crate::musicbrainz::{CacheStats as MusicBrainzCacheStats, MusicBrainzSharedState}; use crate::offline::OfflineState; @@ -40,7 +41,7 @@ use crate::offline_cache::OfflineCacheState; use crate::playback_context::{ContentSource, ContextType, PlaybackContext}; use crate::plex::{PlexMusicSection, PlexPlayResult, PlexServerInfo, PlexTrack}; use crate::queue::QueueTrack; -use crate::reco_store::{RecoEventInput, RecoState}; +use crate::reco_store::{HomeResolved, HomeSeeds, RecoEventInput, RecoState}; use crate::runtime::{RuntimeManagerState, RuntimeStatus, RuntimeError, RuntimeEvent, DegradedReason, CommandRequirement}; use crate::AppState; use md5::{Digest, Md5}; @@ -69,6 +70,23 @@ pub struct AlsaPluginInfo { pub description: String, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(tag = "type", content = "content", rename_all = "lowercase")] +pub enum V2MostPopularItem { + Tracks(Track), + Albums(Album), + Artists(Artist), +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct V2SearchAllResults { + pub albums: SearchResultsPage, + pub tracks: SearchResultsPage, + pub artists: SearchResultsPage, + pub playlists: SearchResultsPage, + pub most_popular: Option, +} + /// Check that Qobuz ToS has been accepted before allowing login. /// /// This is the single enforcement point for ToS gate in backend. @@ -1148,6 +1166,14 @@ pub fn v2_get_favorites_preferences( store.get_preferences() } +#[tauri::command] +pub fn v2_save_favorites_preferences( + prefs: FavoritesPreferences, + state: State<'_, crate::config::favorites_preferences::FavoritesPreferencesState>, +) -> Result { + crate::config::favorites_preferences::save_favorites_preferences(prefs, state) +} + #[tauri::command] pub fn v2_get_cache_stats(state: State<'_, AppState>) -> CacheStats { state.audio_cache.stats() @@ -1421,6 +1447,20 @@ pub fn v2_clear_artist_blacklist( state.clear_all() } +#[tauri::command] +pub fn v2_get_artist_blacklist( + state: State<'_, BlacklistState>, +) -> Result, String> { + state.get_all() +} + +#[tauri::command] +pub fn v2_get_blacklist_settings( + state: State<'_, BlacklistState>, +) -> Result { + state.get_settings() +} + #[tauri::command] pub fn v2_save_credentials(email: String, password: String) -> Result<(), String> { crate::credentials::save_qobuz_credentials(&email, &password) @@ -1519,6 +1559,14 @@ pub async fn v2_cast_stop_discovery(state: State<'_, CastState>) -> Result<(), S discovery.stop_discovery().map_err(|e| e.to_string()) } +#[tauri::command] +pub async fn v2_cast_get_devices( + state: State<'_, CastState>, +) -> Result, String> { + let discovery = state.discovery.lock().await; + Ok(discovery.get_discovered_devices()) +} + #[tauri::command] pub async fn v2_cast_connect(device_id: String, state: State<'_, CastState>) -> Result<(), String> { let device = { @@ -1633,6 +1681,14 @@ pub async fn v2_dlna_stop_discovery(state: State<'_, DlnaState>) -> Result<(), S discovery.stop_discovery().map_err(|e| e.to_string()) } +#[tauri::command] +pub async fn v2_dlna_get_devices( + state: State<'_, DlnaState>, +) -> Result, String> { + let discovery = state.discovery.lock().await; + Ok(discovery.get_discovered_devices()) +} + #[tauri::command] pub async fn v2_dlna_connect(device_id: String, state: State<'_, DlnaState>) -> Result<(), String> { let device = { @@ -1767,6 +1823,14 @@ pub async fn v2_airplay_stop_discovery(state: State<'_, AirPlayState>) -> Result discovery.stop_discovery().map_err(|e| e.to_string()) } +#[tauri::command] +pub async fn v2_airplay_get_devices( + state: State<'_, AirPlayState>, +) -> Result, String> { + let discovery = state.discovery.lock().await; + Ok(discovery.get_discovered_devices()) +} + #[tauri::command] pub async fn v2_airplay_connect( device_id: String, @@ -1953,6 +2017,33 @@ pub async fn v2_library_clear_thumbnails_cache() -> Result { Ok(size_before) } +#[tauri::command] +#[allow(non_snake_case)] +pub async fn v2_library_get_thumbnail(artworkPath: String) -> Result { + crate::library::library_get_thumbnail(artworkPath).await +} + +#[tauri::command] +pub async fn v2_library_get_thumbnails_cache_size() -> Result { + crate::library::library_get_thumbnails_cache_size().await +} + +#[tauri::command] +pub async fn v2_library_get_scan_progress( + library_state: State<'_, LibraryState>, +) -> Result { + crate::library::library_get_scan_progress(library_state).await +} + +#[tauri::command] +#[allow(non_snake_case)] +pub async fn v2_library_get_tracks_by_ids( + trackIds: Vec, + library_state: State<'_, LibraryState>, +) -> Result, String> { + crate::library::library_get_tracks_by_ids(trackIds, library_state).await +} + #[tauri::command] pub async fn v2_library_play_track( track_id: i64, @@ -2280,6 +2371,34 @@ pub fn v2_set_show_network_folders_in_manual_offline( store.set_show_network_folders_in_manual_offline(enabled) } +#[tauri::command] +pub async fn v2_get_offline_status( + state: State<'_, OfflineState>, +) -> Result { + crate::offline::commands::get_offline_status(state).await +} + +#[tauri::command] +pub fn v2_get_offline_settings( + state: State<'_, OfflineState>, +) -> Result { + crate::offline::commands::get_offline_settings(state) +} + +#[tauri::command] +pub async fn v2_set_manual_offline( + enabled: bool, + state: State<'_, OfflineState>, + app_handle: tauri::AppHandle, +) -> Result { + crate::offline::commands::set_manual_offline(enabled, state, app_handle).await +} + +#[tauri::command] +pub async fn v2_check_network() -> bool { + crate::offline::commands::check_network().await +} + #[tauri::command] pub fn v2_add_tracks_to_pending_playlist( pending_id: i64, @@ -2334,6 +2453,13 @@ pub fn v2_mark_scrobbles_sent( store.mark_scrobbles_sent(&ids) } +#[tauri::command] +pub fn v2_get_pending_playlists( + state: State<'_, OfflineState>, +) -> Result, String> { + crate::offline::commands::get_pending_playlists(state) +} + #[tauri::command] pub async fn v2_remove_cached_track( track_id: u64, @@ -2356,6 +2482,25 @@ pub async fn v2_remove_cached_track( Ok(()) } +#[tauri::command] +pub async fn v2_get_cached_tracks( + cache_state: State<'_, OfflineCacheState>, +) -> Result, String> { + let guard = cache_state.db.lock().await; + let db = guard.as_ref().ok_or("No active session - please log in")?; + db.get_all_tracks() +} + +#[tauri::command] +pub async fn v2_get_offline_cache_stats( + cache_state: State<'_, OfflineCacheState>, +) -> Result { + let limit = *cache_state.limit_bytes.lock().await; + let guard = cache_state.db.lock().await; + let db = guard.as_ref().ok_or("No active session - please log in")?; + db.get_stats(&cache_state.get_cache_path(), limit) +} + #[tauri::command] pub async fn v2_set_offline_cache_limit( limit_mb: Option, @@ -2587,6 +2732,64 @@ pub async fn v2_reco_train_scores( Ok(()) } +#[tauri::command] +pub async fn v2_reco_get_home( + limitRecentAlbums: Option, + limitContinueTracks: Option, + limitTopArtists: Option, + limitFavorites: Option, + state: State<'_, RecoState>, +) -> Result { + crate::reco_store::commands::reco_get_home( + limitRecentAlbums, + limitContinueTracks, + limitTopArtists, + limitFavorites, + state, + ) + .await +} + +#[tauri::command] +pub async fn v2_reco_get_home_ml( + limitRecentAlbums: Option, + limitContinueTracks: Option, + limitTopArtists: Option, + limitFavorites: Option, + state: State<'_, RecoState>, +) -> Result { + crate::reco_store::commands::reco_get_home_ml( + limitRecentAlbums, + limitContinueTracks, + limitTopArtists, + limitFavorites, + state, + ) + .await +} + +#[tauri::command] +pub async fn v2_reco_get_home_resolved( + limitRecentAlbums: Option, + limitContinueTracks: Option, + limitTopArtists: Option, + limitFavorites: Option, + reco_state: State<'_, RecoState>, + app_state: State<'_, AppState>, + cache_state: State<'_, ApiCacheState>, +) -> Result { + crate::reco_store::commands::reco_get_home_resolved( + limitRecentAlbums, + limitContinueTracks, + limitTopArtists, + limitFavorites, + reco_state, + app_state, + cache_state, + ) + .await +} + #[derive(Debug, Clone, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct V2LibraryCacheStats { @@ -2694,6 +2897,79 @@ pub async fn v2_playlist_increment_play_count( .map_err(|e| e.to_string()) } +#[tauri::command] +pub async fn v2_playlist_get_all_stats( + state: State<'_, LibraryState>, +) -> Result, String> { + let guard__ = state.db.lock().await; + let db = guard__.as_ref().ok_or("No active session - please log in")?; + db.get_all_playlist_stats().map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn v2_playlist_get_all_local_track_counts( + state: State<'_, LibraryState>, +) -> Result, String> { + let guard__ = state.db.lock().await; + let db = guard__.as_ref().ok_or("No active session - please log in")?; + db.get_all_playlist_local_track_counts() + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn v2_get_playlist_folders( + state: State<'_, LibraryState>, +) -> Result, String> { + let guard__ = state.db.lock().await; + let db = guard__.as_ref().ok_or("No active session - please log in")?; + db.get_all_playlist_folders().map_err(|e| e.to_string()) +} + +#[tauri::command] +#[allow(non_snake_case)] +pub async fn v2_create_playlist_folder( + name: String, + iconType: Option, + iconPreset: Option, + iconColor: Option, + state: State<'_, LibraryState>, +) -> Result { + let guard__ = state.db.lock().await; + let db = guard__.as_ref().ok_or("No active session - please log in")?; + db.create_playlist_folder( + &name, + iconType.as_deref(), + iconPreset.as_deref(), + iconColor.as_deref(), + ) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +#[allow(non_snake_case)] +pub async fn v2_update_playlist_folder( + id: String, + name: Option, + iconType: Option, + iconPreset: Option, + iconColor: Option, + customImagePath: Option, + isHidden: Option, + state: State<'_, LibraryState>, +) -> Result { + crate::library::update_playlist_folder( + id, + name, + iconType, + iconPreset, + iconColor, + customImagePath, + isHidden, + state, + ) + .await +} + #[tauri::command] pub async fn v2_library_get_albums( include_hidden: Option, @@ -2720,6 +2996,91 @@ pub async fn v2_library_get_albums( .map_err(|e| e.to_string()) } +#[tauri::command] +pub async fn v2_library_get_stats( + state: State<'_, LibraryState>, + download_settings_state: State<'_, DownloadSettingsState>, +) -> Result { + crate::library::library_get_stats(state, download_settings_state).await +} + +#[tauri::command] +pub async fn v2_library_get_folders( + state: State<'_, LibraryState>, +) -> Result, String> { + crate::library::library_get_folders(state).await +} + +#[tauri::command] +pub async fn v2_library_get_folders_with_metadata( + state: State<'_, LibraryState>, +) -> Result, String> { + crate::library::library_get_folders_with_metadata(state).await +} + +#[tauri::command] +pub async fn v2_library_add_folder( + path: String, + state: State<'_, LibraryState>, +) -> Result { + crate::library::library_add_folder(path, state).await +} + +#[tauri::command] +pub async fn v2_library_cleanup_missing_files( + state: State<'_, LibraryState>, +) -> Result { + crate::library::library_cleanup_missing_files(state).await +} + +#[tauri::command] +pub async fn v2_library_fetch_missing_artwork( + state: State<'_, LibraryState>, +) -> Result { + crate::library::library_fetch_missing_artwork(state).await +} + +#[tauri::command] +pub async fn v2_library_get_artists( + exclude_network_folders: Option, + state: State<'_, LibraryState>, + download_settings_state: State<'_, DownloadSettingsState>, +) -> Result, String> { + crate::library::library_get_artists( + exclude_network_folders, + state, + download_settings_state, + ) + .await +} + +#[tauri::command] +pub async fn v2_library_search( + query: String, + limit: Option, + exclude_network_folders: Option, + state: State<'_, LibraryState>, + download_settings_state: State<'_, DownloadSettingsState>, +) -> Result, String> { + crate::library::library_search( + query, + limit, + exclude_network_folders, + state, + download_settings_state, + ) + .await +} + +#[tauri::command] +#[allow(non_snake_case)] +pub async fn v2_library_get_album_tracks( + albumGroupKey: String, + state: State<'_, LibraryState>, +) -> Result, String> { + crate::library::library_get_album_tracks(albumGroupKey, state).await +} + #[tauri::command] #[allow(non_snake_case)] pub async fn v2_plex_get_music_sections( @@ -3430,6 +3791,164 @@ pub async fn v2_search_artists( Ok(results) } +/// Search all categories in one call (albums/tracks/artists/playlists + most_popular) +#[tauri::command] +pub async fn v2_search_all( + query: String, + state: State<'_, AppState>, + blacklist_state: State<'_, BlacklistState>, + runtime: State<'_, RuntimeManagerState>, +) -> Result { + runtime.manager().check_requirements(CommandRequirement::RequiresUserSession).await?; + + let url = crate::api::endpoints::build_url(crate::api::endpoints::paths::CATALOG_SEARCH); + let response: serde_json::Value = { + let client = state.client.read().await; + client + .get_http() + .get(&url) + .header( + "X-App-Id", + client + .app_id() + .await + .map_err(|e| RuntimeError::Internal(e.to_string()))?, + ) + .header( + "X-User-Auth-Token", + client + .auth_token() + .await + .map_err(|e| RuntimeError::Internal(e.to_string()))?, + ) + .query(&[("query", query.as_str()), ("limit", "30"), ("offset", "0")]) + .send() + .await + .map_err(|e| RuntimeError::Internal(format!("Request failed: {}", e)))? + .json() + .await + .map_err(|e| RuntimeError::Internal(format!("JSON parse failed: {}", e)))? + }; + + let mut albums: SearchResultsPage = response + .get("albums") + .and_then(|v| serde_json::from_value(v.clone()).ok()) + .unwrap_or_else(|| SearchResultsPage { + items: vec![], + total: 0, + offset: 0, + limit: 30, + }); + let mut tracks: SearchResultsPage = response + .get("tracks") + .and_then(|v| serde_json::from_value(v.clone()).ok()) + .unwrap_or_else(|| SearchResultsPage { + items: vec![], + total: 0, + offset: 0, + limit: 30, + }); + let mut artists: SearchResultsPage = response + .get("artists") + .and_then(|v| serde_json::from_value(v.clone()).ok()) + .unwrap_or_else(|| SearchResultsPage { + items: vec![], + total: 0, + offset: 0, + limit: 30, + }); + let playlists: SearchResultsPage = response + .get("playlists") + .and_then(|v| serde_json::from_value(v.clone()).ok()) + .unwrap_or_else(|| SearchResultsPage { + items: vec![], + total: 0, + offset: 0, + limit: 30, + }); + + let most_popular: Option = response + .get("most_popular") + .and_then(|mp| mp.get("items")) + .and_then(|items| items.as_array()) + .and_then(|arr| { + for item in arr { + let item_type = item.get("type").and_then(|t| t.as_str())?; + let content = item.get("content")?; + + match item_type { + "tracks" => { + if let Ok(track) = serde_json::from_value::(content.clone()) { + if let Some(ref performer) = track.performer { + if blacklist_state.is_blacklisted(performer.id) { + continue; + } + } + return Some(V2MostPopularItem::Tracks(track)); + } + } + "albums" => { + if let Ok(album) = serde_json::from_value::(content.clone()) { + if blacklist_state.is_blacklisted(album.artist.id) { + continue; + } + return Some(V2MostPopularItem::Albums(album)); + } + } + "artists" => { + if let Ok(artist) = serde_json::from_value::(content.clone()) { + if blacklist_state.is_blacklisted(artist.id) { + continue; + } + return Some(V2MostPopularItem::Artists(artist)); + } + } + _ => {} + } + } + None + }); + + let original_album_count = albums.items.len(); + albums + .items + .retain(|album| !blacklist_state.is_blacklisted(album.artist.id)); + let filtered_albums = original_album_count - albums.items.len(); + if filtered_albums > 0 { + albums.total = albums.total.saturating_sub(filtered_albums as u32); + } + + let original_track_count = tracks.items.len(); + tracks.items.retain(|track| { + if let Some(ref performer) = track.performer { + !blacklist_state.is_blacklisted(performer.id) + } else { + true + } + }); + let filtered_tracks = original_track_count - tracks.items.len(); + if filtered_tracks > 0 { + tracks.total = tracks.total.saturating_sub(filtered_tracks as u32); + } + + let original_artist_count = artists.items.len(); + artists + .items + .retain(|artist| !blacklist_state.is_blacklisted(artist.id)); + let filtered_artists = original_artist_count - artists.items.len(); + if filtered_artists > 0 { + artists.total = artists.total.saturating_sub(filtered_artists as u32); + } + + Ok(V2SearchAllResults { + albums, + tracks, + artists, + playlists, + most_popular, + }) +} + // ==================== Catalog Commands (V2) ==================== /// Get album by ID (V2 - uses QbzCore) @@ -4061,6 +4580,26 @@ pub async fn v2_get_playlist( bridge.get_playlist(playlistId).await.map_err(RuntimeError::Internal) } +/// Get playlist metadata + track ids for progressive loading. +#[tauri::command] +#[allow(non_snake_case)] +pub async fn v2_get_playlist_track_ids( + playlistId: u64, + app_state: State<'_, AppState>, + runtime: State<'_, RuntimeManagerState>, +) -> Result { + runtime + .manager() + .check_requirements(CommandRequirement::RequiresUserSession) + .await?; + + let client = app_state.client.read().await; + client + .get_playlist_track_ids(playlistId) + .await + .map_err(|e| RuntimeError::Internal(e.to_string())) +} + /// Add tracks to playlist (V2 - uses QbzCore) #[tauri::command] #[allow(non_snake_case)] @@ -4446,6 +4985,45 @@ pub async fn v2_update_playlist( bridge.update_playlist(playlistId, name.as_deref(), description.as_deref(), isPublic).await.map_err(RuntimeError::Internal) } +#[tauri::command] +#[allow(non_snake_case)] +pub async fn v2_playlist_get_custom_order( + playlistId: u64, + library_state: State<'_, LibraryState>, + runtime: State<'_, RuntimeManagerState>, +) -> Result, RuntimeError> { + runtime.manager().check_requirements(CommandRequirement::RequiresUserSession).await?; + crate::library::playlist_get_custom_order(playlistId, library_state) + .await + .map_err(RuntimeError::Internal) +} + +#[tauri::command] +#[allow(non_snake_case)] +pub async fn v2_playlist_has_custom_order( + playlistId: u64, + library_state: State<'_, LibraryState>, + runtime: State<'_, RuntimeManagerState>, +) -> Result { + runtime.manager().check_requirements(CommandRequirement::RequiresUserSession).await?; + crate::library::playlist_has_custom_order(playlistId, library_state) + .await + .map_err(RuntimeError::Internal) +} + +#[tauri::command] +#[allow(non_snake_case)] +pub async fn v2_playlist_get_tracks_with_local_copies( + trackIds: Vec, + library_state: State<'_, LibraryState>, + runtime: State<'_, RuntimeManagerState>, +) -> Result, RuntimeError> { + runtime.manager().check_requirements(CommandRequirement::RequiresUserSession).await?; + crate::library::playlist_get_tracks_with_local_copies(trackIds, library_state) + .await + .map_err(RuntimeError::Internal) +} + /// Search playlists (V2 - uses QbzCore) #[tauri::command] pub async fn v2_search_playlists( @@ -4905,6 +5483,190 @@ pub async fn v2_musicbrainz_resolve_artist( .map_err(|e| RuntimeError::Internal(e.to_string())) } +#[tauri::command] +pub async fn v2_resolve_musician( + name: String, + role: String, + mb_state: State<'_, MusicBrainzV2State>, + bridge: State<'_, CoreBridgeState>, + runtime: State<'_, RuntimeManagerState>, +) -> Result { + runtime + .manager() + .check_requirements(CommandRequirement::RequiresCoreBridgeAuth) + .await?; + + let resolved_artist = { + let client = mb_state.client.lock().await; + client + .resolve_artist(&name) + .await + .map_err(|e| RuntimeError::Internal(e.to_string()))? + }; + + let normalized_target = name.trim().to_lowercase(); + let bridge = bridge.get().await; + let artist_results = bridge + .search_artists(&name, 10, 0, None) + .await + .map_err(RuntimeError::Internal)?; + let exact = artist_results + .items + .iter() + .find(|artist| artist.name.trim().to_lowercase() == normalized_target); + + if let Some(artist) = exact { + let qobuz_artist_id = i64::try_from(artist.id).ok(); + return Ok(crate::musicbrainz::ResolvedMusician { + name, + role, + mbid: None, + qobuz_artist_id, + confidence: crate::musicbrainz::MusicianConfidence::Confirmed, + bands: Vec::new(), + appears_on_count: 0, + }); + } + + let album_results = bridge + .search_albums(&name, 20, 0, None) + .await + .map_err(RuntimeError::Internal)?; + let appears_on_count = album_results.total as usize; + + let confidence = if appears_on_count > 0 { + crate::musicbrainz::MusicianConfidence::Contextual + } else if resolved_artist.is_some() { + crate::musicbrainz::MusicianConfidence::Weak + } else { + crate::musicbrainz::MusicianConfidence::None + }; + + Ok(crate::musicbrainz::ResolvedMusician { + name, + role, + mbid: resolved_artist.as_ref().map(|a| a.mbid.clone()), + qobuz_artist_id: None, + confidence, + bands: Vec::new(), + appears_on_count, + }) +} + +#[tauri::command] +pub async fn v2_get_musician_appearances( + name: String, + role: String, + limit: Option, + offset: Option, + bridge: State<'_, CoreBridgeState>, + runtime: State<'_, RuntimeManagerState>, +) -> Result { + runtime + .manager() + .check_requirements(CommandRequirement::RequiresCoreBridgeAuth) + .await?; + + let bridge = bridge.get().await; + let results = bridge + .search_albums(&name, limit.unwrap_or(20), offset.unwrap_or(0), None) + .await + .map_err(RuntimeError::Internal)?; + + let albums = results + .items + .into_iter() + .map(|album| crate::musicbrainz::AlbumAppearance { + album_id: album.id, + album_title: album.title, + album_artwork: album.image.large.or(album.image.small).unwrap_or_default(), + artist_name: album.artist.name, + year: album.release_date_original, + role_on_album: role.clone(), + }) + .collect::>(); + + Ok(crate::musicbrainz::MusicianAppearances { + albums, + total: results.total as usize, + }) +} + +#[tauri::command] +pub async fn v2_musicbrainz_get_artist_relationships( + mbid: String, + state: State<'_, MusicBrainzSharedState>, +) -> Result { + // Check cache first + { + let cache_opt = state.cache.lock().await; + let cache = cache_opt.as_ref().ok_or("No active session - please log in")?; + if let Some(cached) = cache.get_artist_relations(&mbid)? { + return Ok(cached); + } + } + + let artist = state.client.get_artist_with_relations(&mbid).await?; + + let mut members = Vec::new(); + let mut past_members = Vec::new(); + let mut groups = Vec::new(); + let mut collaborators = Vec::new(); + + if let Some(relations) = &artist.relations { + for relation in relations { + let Some(related_artist) = &relation.artist else { + continue; + }; + + let related = crate::musicbrainz::RelatedArtist { + mbid: related_artist.id.clone(), + name: related_artist.name.clone(), + role: relation.attributes.as_ref().and_then(|a| a.first().cloned()), + period: Some(crate::musicbrainz::Period { + begin: relation.begin.clone(), + end: relation.end.clone(), + }), + ended: relation.ended.unwrap_or(false), + }; + + match relation.relation_type.as_str() { + "member of band" => { + if relation.direction.as_deref() == Some("backward") { + if related.ended { + past_members.push(related); + } else { + members.push(related); + } + } else { + groups.push(related); + } + } + "collaboration" => { + collaborators.push(related); + } + _ => {} + } + } + } + + let result = crate::musicbrainz::ArtistRelationships { + members, + past_members, + groups, + collaborators, + }; + + // Cache result + { + let cache_opt = state.cache.lock().await; + let cache = cache_opt.as_ref().ok_or("No active session - please log in")?; + cache.set_artist_relations(&mbid, &result)?; + } + + Ok(result) +} + // --- Last.fm V2 --- /// Get Last.fm auth token and URL (V2) @@ -5907,6 +6669,36 @@ pub fn v2_set_show_whats_new_on_launch( crate::updates::set_show_whats_new_on_launch(enabled, state) } +#[tauri::command] +pub fn v2_get_update_preferences( + state: State<'_, crate::updates::UpdatesState>, +) -> Result { + crate::updates::get_update_preferences(state) +} + +#[tauri::command] +pub fn v2_get_current_version( + state: State<'_, crate::updates::UpdatesState>, +) -> String { + crate::updates::get_current_version(state) +} + +#[tauri::command] +pub async fn v2_check_for_updates( + mode: String, + state: State<'_, crate::updates::UpdatesState>, +) -> Result { + crate::updates::check_for_updates(mode, state).await +} + +#[tauri::command] +pub async fn v2_fetch_release_for_version( + version: String, + state: State<'_, crate::updates::UpdatesState>, +) -> Result, String> { + crate::updates::fetch_release_for_version(version, state).await +} + #[tauri::command] pub fn v2_acknowledge_release( version: String, @@ -5931,6 +6723,14 @@ pub fn v2_mark_whats_new_shown( crate::updates::mark_whats_new_shown(version, state) } +#[tauri::command] +pub fn v2_has_whats_new_been_shown( + version: String, + state: State<'_, crate::updates::UpdatesState>, +) -> Result { + crate::updates::has_whats_new_been_shown(version, state) +} + #[tauri::command] pub fn v2_mark_flatpak_welcome_shown( state: State<'_, crate::updates::UpdatesState>, @@ -5956,6 +6756,96 @@ pub fn v2_set_show_downloads_in_library( crate::config::download_settings::set_show_downloads_in_library(show, state) } +#[tauri::command] +pub fn v2_get_download_settings( + state: State<'_, crate::config::download_settings::DownloadSettingsState>, +) -> Result { + crate::config::download_settings::get_download_settings(state) +} + +#[tauri::command] +pub async fn v2_lyrics_get_cache_stats( + state: State<'_, crate::lyrics::LyricsState>, +) -> Result { + crate::lyrics::commands::lyrics_get_cache_stats(state).await +} + +#[tauri::command] +pub fn v2_lastfm_has_embedded_credentials() -> bool { + crate::lastfm::LastFmClient::has_embedded_credentials() +} + +#[tauri::command] +pub async fn v2_remote_control_get_status( + app_handle: tauri::AppHandle, +) -> Result { + crate::api_server::remote_control_get_status(app_handle).await +} + +#[tauri::command] +pub async fn v2_remote_control_set_enabled( + enabled: bool, + app_handle: tauri::AppHandle, +) -> Result { + crate::api_server::remote_control_set_enabled(enabled, app_handle).await +} + +#[tauri::command] +pub async fn v2_remote_control_set_port( + port: u16, + app_handle: tauri::AppHandle, +) -> Result { + crate::api_server::remote_control_set_port(port, app_handle).await +} + +#[tauri::command] +pub async fn v2_remote_control_set_secure( + secure: bool, + app_handle: tauri::AppHandle, +) -> Result { + crate::api_server::remote_control_set_secure(secure, app_handle).await +} + +#[tauri::command] +pub async fn v2_remote_control_regenerate_token( + app_handle: tauri::AppHandle, +) -> Result { + crate::api_server::remote_control_regenerate_token(app_handle).await +} + +#[tauri::command] +pub async fn v2_remote_control_get_pairing_qr( + app_handle: tauri::AppHandle, +) -> Result { + crate::api_server::remote_control_get_pairing_qr(app_handle).await +} + +#[tauri::command] +pub fn v2_is_running_in_flatpak() -> bool { + crate::flatpak::is_running_in_flatpak() +} + +#[tauri::command] +pub async fn v2_detect_legacy_cached_files( + cache_state: State<'_, OfflineCacheState>, +) -> Result { + let tracks_dir = cache_state.cache_dir.read().unwrap().join("tracks"); + let track_ids = crate::offline_cache::detect_legacy_cached_files(&tracks_dir)?; + Ok(crate::offline_cache::MigrationStatus { + has_legacy_files: !track_ids.is_empty(), + total_tracks: track_ids.len(), + ..Default::default() + }) +} + +#[tauri::command] +pub fn v2_get_device_sample_rate_limit( + state: State<'_, crate::config::audio_settings::AudioSettingsState>, + device_id: String, +) -> Result, String> { + crate::config::audio_settings::get_device_sample_rate_limit(state, device_id) +} + #[tauri::command] pub fn v2_set_device_sample_rate_limit( state: State<'_, crate::config::audio_settings::AudioSettingsState>, diff --git a/src-tauri/src/config/favorites_cache.rs b/src-tauri/src/config/favorites_cache.rs index 2458a084f..c6633b16e 100644 --- a/src-tauri/src/config/favorites_cache.rs +++ b/src-tauri/src/config/favorites_cache.rs @@ -529,3 +529,17 @@ pub fn clear_favorites_cache(state: tauri::State) -> Result let store = guard.as_ref().ok_or("No active session - please log in")?; store.clear_all() } + +/// Check if a track is cached as favorite. +#[tauri::command] +pub fn is_track_favorite( + track_id: i64, + state: tauri::State, +) -> Result { + let guard = state + .store + .lock() + .map_err(|_| "Failed to lock favorites cache store".to_string())?; + let store = guard.as_ref().ok_or("No active session - please log in")?; + store.is_track_favorite(track_id) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 53609db7a..84754da3a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -600,6 +600,32 @@ pub fn run() { commands_v2::v2_get_user_info, commands_v2::v2_save_credentials, commands_v2::v2_clear_saved_credentials, + // Temporary compatibility commands still invoked by frontend during migration + offline_cache::commands::check_album_fully_cached, + network::commands::check_network_path, + offline::commands::create_pending_playlist, + offline::commands::get_pending_playlist_count, + offline::commands::queue_scrobble, + offline::commands::get_queued_scrobbles, + offline::commands::get_queued_scrobble_count, + offline::commands::cleanup_sent_scrobbles, + library::commands::discogs_has_credentials, + library::commands::discogs_search_artwork, + library::commands::discogs_download_artwork, + library::commands::library_update_folder_settings, + library::commands::get_track_by_path, + commands::search::get_artist_basic, + commands::search::get_artist_detail, + commands::search::get_artist_tracks, + commands::search::get_artist_albums, + commands::search::get_releases_grid, + commands::playback::get_pipewire_sinks, + commands::playback::get_audio_output_status, + flatpak::get_flatpak_help_text, + config::legal_settings::get_qobuz_tos_accepted, + updates::has_flatpak_welcome_been_shown, + lyrics::commands::lyrics_get, + config::favorites_cache::is_track_favorite, commands_v2::v2_set_api_locale, commands_v2::v2_set_use_system_titlebar, commands_v2::v2_set_enable_tray, @@ -610,6 +636,7 @@ pub fn run() { commands_v2::v2_set_show_context_icon, commands_v2::v2_get_playback_preferences, commands_v2::v2_get_favorites_preferences, + commands_v2::v2_save_favorites_preferences, commands_v2::v2_get_cache_stats, commands_v2::v2_get_available_backends, commands_v2::v2_get_devices_for_backend, @@ -636,6 +663,8 @@ pub fn run() { commands_v2::v2_add_to_artist_blacklist, commands_v2::v2_remove_from_artist_blacklist, commands_v2::v2_set_blacklist_enabled, + commands_v2::v2_get_artist_blacklist, + commands_v2::v2_get_blacklist_settings, commands_v2::v2_clear_artist_blacklist, commands_v2::v2_plex_open_auth_url, commands_v2::v2_plex_cache_get_sections, @@ -650,6 +679,7 @@ pub fn run() { commands_v2::v2_plex_cache_clear, commands_v2::v2_cast_start_discovery, commands_v2::v2_cast_stop_discovery, + commands_v2::v2_cast_get_devices, commands_v2::v2_cast_connect, commands_v2::v2_cast_disconnect, commands_v2::v2_cast_play_track, @@ -660,6 +690,7 @@ pub fn run() { commands_v2::v2_cast_set_volume, commands_v2::v2_dlna_start_discovery, commands_v2::v2_dlna_stop_discovery, + commands_v2::v2_dlna_get_devices, commands_v2::v2_dlna_connect, commands_v2::v2_dlna_disconnect, commands_v2::v2_dlna_play_track, @@ -670,6 +701,7 @@ pub fn run() { commands_v2::v2_dlna_set_volume, commands_v2::v2_airplay_start_discovery, commands_v2::v2_airplay_stop_discovery, + commands_v2::v2_airplay_get_devices, commands_v2::v2_airplay_connect, commands_v2::v2_airplay_disconnect, commands_v2::v2_airplay_load_media, @@ -682,6 +714,10 @@ pub fn run() { commands_v2::v2_library_check_folder_accessible, commands_v2::v2_library_clear_artwork_cache, commands_v2::v2_library_clear_thumbnails_cache, + commands_v2::v2_library_get_thumbnail, + commands_v2::v2_library_get_thumbnails_cache_size, + commands_v2::v2_library_get_scan_progress, + commands_v2::v2_library_get_tracks_by_ids, commands_v2::v2_library_play_track, commands_v2::v2_playlist_set_sort, commands_v2::v2_playlist_set_artwork, @@ -691,6 +727,8 @@ pub fn run() { commands_v2::v2_playlist_get_settings, commands_v2::v2_playlist_get_stats, commands_v2::v2_playlist_increment_play_count, + commands_v2::v2_playlist_get_all_stats, + commands_v2::v2_playlist_get_all_local_track_counts, commands_v2::v2_playlist_add_local_track, commands_v2::v2_playlist_remove_local_track, commands_v2::v2_playlist_set_hidden, @@ -699,13 +737,20 @@ pub fn run() { commands_v2::v2_playlist_init_custom_order, commands_v2::v2_playlist_set_custom_order, commands_v2::v2_playlist_move_track, + commands_v2::v2_playlist_get_custom_order, + commands_v2::v2_playlist_has_custom_order, + commands_v2::v2_playlist_get_tracks_with_local_copies, commands_v2::v2_library_set_album_artwork, commands_v2::v2_library_set_album_hidden, commands_v2::v2_create_track_radio, commands_v2::v2_delete_playlist_folder, commands_v2::v2_reorder_playlist_folders, commands_v2::v2_move_playlist_to_folder, + commands_v2::v2_get_playlist_folders, + commands_v2::v2_create_playlist_folder, + commands_v2::v2_update_playlist_folder, commands_v2::v2_lyrics_clear_cache, + commands_v2::v2_lyrics_get_cache_stats, commands_v2::v2_musicbrainz_get_cache_stats, commands_v2::v2_musicbrainz_clear_cache, commands_v2::v2_set_show_partial_playlists, @@ -713,22 +758,50 @@ pub fn run() { commands_v2::v2_set_allow_immediate_scrobbling, commands_v2::v2_set_allow_accumulated_scrobbling, commands_v2::v2_set_show_network_folders_in_manual_offline, + commands_v2::v2_get_offline_status, + commands_v2::v2_get_offline_settings, + commands_v2::v2_set_manual_offline, + commands_v2::v2_check_network, commands_v2::v2_add_tracks_to_pending_playlist, + commands_v2::v2_get_pending_playlists, commands_v2::v2_update_pending_playlist_qobuz_id, commands_v2::v2_mark_pending_playlist_synced, commands_v2::v2_delete_pending_playlist, commands_v2::v2_mark_scrobbles_sent, commands_v2::v2_remove_cached_track, + commands_v2::v2_get_cached_tracks, + commands_v2::v2_get_offline_cache_stats, commands_v2::v2_set_offline_cache_limit, commands_v2::v2_open_offline_cache_folder, commands_v2::v2_open_album_folder, commands_v2::v2_open_track_folder, commands_v2::v2_lastfm_open_auth_url, commands_v2::v2_lastfm_set_credentials, + commands_v2::v2_lastfm_has_embedded_credentials, + commands_v2::v2_remote_control_get_status, + commands_v2::v2_remote_control_set_enabled, + commands_v2::v2_remote_control_set_port, + commands_v2::v2_remote_control_set_secure, + commands_v2::v2_remote_control_regenerate_token, + commands_v2::v2_remote_control_get_pairing_qr, + commands_v2::v2_is_running_in_flatpak, + commands_v2::v2_detect_legacy_cached_files, commands_v2::v2_reco_log_event, commands_v2::v2_reco_train_scores, + commands_v2::v2_reco_get_home, + commands_v2::v2_reco_get_home_ml, + commands_v2::v2_reco_get_home_resolved, commands_v2::v2_library_get_cache_stats, + commands_v2::v2_library_get_stats, commands_v2::v2_library_get_albums, + commands_v2::v2_library_get_folders, + commands_v2::v2_library_get_folders_with_metadata, + commands_v2::v2_library_add_folder, + commands_v2::v2_library_cleanup_missing_files, + commands_v2::v2_library_fetch_missing_artwork, + commands_v2::v2_library_get_artists, + commands_v2::v2_library_search, + commands_v2::v2_library_get_album_tracks, commands_v2::v2_library_update_folder_path, commands_v2::v2_library_cache_artist_image, commands_v2::v2_library_set_custom_artist_image, @@ -745,15 +818,22 @@ pub fn run() { commands_v2::v2_library_refresh_album_metadata_from_files, commands_v2::v2_factory_reset, commands_v2::v2_set_qobuz_tos_accepted, + commands_v2::v2_get_update_preferences, + commands_v2::v2_get_current_version, + commands_v2::v2_check_for_updates, + commands_v2::v2_fetch_release_for_version, commands_v2::v2_set_update_check_on_launch, commands_v2::v2_set_show_whats_new_on_launch, commands_v2::v2_acknowledge_release, commands_v2::v2_ignore_release, + commands_v2::v2_has_whats_new_been_shown, commands_v2::v2_mark_whats_new_shown, commands_v2::v2_mark_flatpak_welcome_shown, commands_v2::v2_get_backend_logs, commands_v2::v2_upload_logs_to_paste, + commands_v2::v2_get_download_settings, commands_v2::v2_set_show_downloads_in_library, + commands_v2::v2_get_device_sample_rate_limit, commands_v2::v2_set_device_sample_rate_limit, commands_v2::v2_set_force_x11, commands_v2::v2_restart_app, @@ -776,6 +856,7 @@ pub fn run() { commands_v2::v2_search_albums, commands_v2::v2_search_tracks, commands_v2::v2_search_artists, + commands_v2::v2_search_all, commands_v2::v2_get_album, commands_v2::v2_get_track, commands_v2::v2_get_artist, @@ -784,6 +865,7 @@ pub fn run() { commands_v2::v2_remove_favorite, commands_v2::v2_get_user_playlists, commands_v2::v2_get_playlist, + commands_v2::v2_get_playlist_track_ids, commands_v2::v2_add_tracks_to_playlist, commands_v2::v2_remove_tracks_from_playlist, commands_v2::v2_create_playlist, @@ -840,6 +922,9 @@ pub fn run() { commands_v2::v2_musicbrainz_set_enabled, commands_v2::v2_musicbrainz_resolve_track, commands_v2::v2_musicbrainz_resolve_artist, + commands_v2::v2_resolve_musician, + commands_v2::v2_get_musician_appearances, + commands_v2::v2_musicbrainz_get_artist_relationships, commands_v2::v2_lastfm_get_auth_url, commands_v2::v2_lastfm_complete_auth, commands_v2::v2_lastfm_is_authenticated, diff --git a/src-tauri/src/library/mod.rs b/src-tauri/src/library/mod.rs index 79a9a0f97..950bd7736 100644 --- a/src-tauri/src/library/mod.rs +++ b/src-tauri/src/library/mod.rs @@ -55,10 +55,28 @@ pub mod thumbnails { pub use commands::LibraryState; pub use commands::{ LibraryAlbumMetadataUpdateRequest, + CleanupResult, + library_add_folder, + library_cleanup_missing_files, + library_fetch_missing_artwork, + library_get_album_tracks, + library_get_artists, + library_get_folders, + library_get_folders_with_metadata, library_scan_impl, + library_get_scan_progress, + library_get_stats, + library_get_thumbnail, + library_get_thumbnails_cache_size, + library_get_tracks_by_ids, + library_search, library_stop_scan_impl, library_scan_folder_impl, library_clear_impl, + playlist_get_custom_order, + playlist_get_tracks_with_local_copies, + playlist_has_custom_order, + update_playlist_folder, library_update_album_metadata_impl, library_write_album_metadata_to_files_impl, library_refresh_album_metadata_from_files_impl, diff --git a/src/lib/components/views/ArtistDetailView.svelte b/src/lib/components/views/ArtistDetailView.svelte index 7149bfa3b..8ea92871d 100644 --- a/src/lib/components/views/ArtistDetailView.svelte +++ b/src/lib/components/views/ArtistDetailView.svelte @@ -690,7 +690,7 @@ mbid?: string; name?: string; confidence: string; - }>('musicbrainz_resolve_artist', { name: artist.name }); + }>('v2_musicbrainz_resolve_artist', { name: artist.name }); if (!resolved?.mbid || resolved.confidence === 'none') { mbRelationshipsLoading = false; @@ -705,7 +705,7 @@ past_members: RelatedArtist[]; groups: RelatedArtist[]; collaborators: RelatedArtist[]; - }>('musicbrainz_get_artist_relationships', { mbid: resolved.mbid }); + }>('v2_musicbrainz_get_artist_relationships', { mbid: resolved.mbid }); mbRelationships = { members: relationships.members || [], diff --git a/src/lib/components/views/HomeView.svelte b/src/lib/components/views/HomeView.svelte index 0d9681d5f..c9698225c 100644 --- a/src/lib/components/views/HomeView.svelte +++ b/src/lib/components/views/HomeView.svelte @@ -793,7 +793,7 @@ const discoverPromise = fetchAllDiscoverData(genreIds); // Single IPC call returns fully-resolved card data (3-tier cache in Rust) - const mlPromise = invoke('reco_get_home_resolved', { + const mlPromise = invoke('v2_reco_get_home_resolved', { limitRecentAlbums: homeLimits.recentAlbums, limitContinueTracks: homeLimits.continueTracks, limitTopArtists: homeLimits.topArtists, diff --git a/src/lib/components/views/LocalLibraryView.svelte b/src/lib/components/views/LocalLibraryView.svelte index 620f57319..e77b5af19 100644 --- a/src/lib/components/views/LocalLibraryView.svelte +++ b/src/lib/components/views/LocalLibraryView.svelte @@ -1289,7 +1289,7 @@ includeHidden: false, excludeNetworkFolders: excludeNetwork }), - invoke('library_get_stats'), + invoke('v2_library_get_stats'), includePlex ? invoke('v2_plex_cache_get_albums').catch(() => []) : Promise.resolve([]) @@ -1354,7 +1354,7 @@ if (isOffline) { // When offline, get folders without calling is_network_path (blocks offline) // Use basic folder list command instead - folders = await invoke('library_get_folders'); + folders = await invoke('v2_library_get_folders_with_metadata'); console.log('[LocalLibrary] Received folders (offline mode):', folders.length); // Mark all network folders as inaccessible, local folders as accessible @@ -1367,7 +1367,7 @@ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Folder loading timeout')), 5000) ); - const foldersPromise = invoke('library_get_folders_with_metadata'); + const foldersPromise = invoke('v2_library_get_folders_with_metadata'); folders = await Promise.race([foldersPromise, timeoutPromise]); console.log('[LocalLibrary] Received folders (online mode):', folders.length); @@ -1407,7 +1407,7 @@ console.log('[LocalLibrary] Calling library_get_artists + plex_cache_get_albums'); const includePlex = isPlexLibraryEnabled(); const [localArtists, plexAlbumsRaw] = await Promise.all([ - invoke('library_get_artists', { + invoke('v2_library_get_artists', { excludeNetworkFolders: shouldExcludeNetworkFolders() }), includePlex @@ -1469,7 +1469,7 @@ console.log('[LocalLibrary] Calling library_search + plex_cache_search_tracks'); const includePlex = isPlexLibraryEnabled(); const [localTracks, plexTracksRaw] = await Promise.all([ - invoke('library_search', { + invoke('v2_library_search', { query, limit: 0, // 0 = no limit, virtualization handles any list size excludeNetworkFolders: shouldExcludeNetworkFolders() @@ -1534,7 +1534,7 @@ if (!selected || typeof selected !== 'string') return; - const newFolder = await invoke('library_add_folder', { path: selected }); + const newFolder = await invoke('v2_library_add_folder', { path: selected }); await loadFolders(); // Show warning if network folder detected @@ -1694,7 +1694,7 @@ cleaningUpMissingFiles = true; cleanupStatus = 'Scanning track paths...'; try { - const result = await invoke<{ checked: number; removed: number }>('library_cleanup_missing_files'); + const result = await invoke<{ checked: number; removed: number }>('v2_library_cleanup_missing_files'); if (result.removed > 0) { cleanupStatus = `Removed ${result.removed} of ${result.checked} tracks`; @@ -1783,7 +1783,7 @@ fetchingArtwork = true; try { - const count = await invoke('library_fetch_missing_artwork'); + const count = await invoke('v2_library_fetch_missing_artwork'); if (count > 0) { alert(`Fetched artwork for ${count} albums from Discogs.`); await loadLibraryData(); @@ -1972,7 +1972,7 @@ return await hydratePlexTrackQuality(mappedTracks); } - return await invoke('library_get_album_tracks', { + return await invoke('v2_library_get_album_tracks', { albumGroupKey: album.id }); } catch (err) { diff --git a/src/lib/components/views/MusicianPageView.svelte b/src/lib/components/views/MusicianPageView.svelte index 8761ef369..6b17749ef 100644 --- a/src/lib/components/views/MusicianPageView.svelte +++ b/src/lib/components/views/MusicianPageView.svelte @@ -47,7 +47,7 @@ error = null; try { - const result = await invoke('get_musician_appearances', { + const result = await invoke('v2_get_musician_appearances', { name: musician.name, role: musician.role, limit: ITEMS_PER_PAGE, @@ -71,7 +71,7 @@ isLoadingMore = true; try { - const result = await invoke('get_musician_appearances', { + const result = await invoke('v2_get_musician_appearances', { name: musician.name, role: musician.role, limit: ITEMS_PER_PAGE, diff --git a/src/lib/components/views/PlaylistDetailView.svelte b/src/lib/components/views/PlaylistDetailView.svelte index 440c13da2..f70a939f1 100644 --- a/src/lib/components/views/PlaylistDetailView.svelte +++ b/src/lib/components/views/PlaylistDetailView.svelte @@ -602,7 +602,7 @@ if (pending.trackIds.length > 0) { try { - const qobuzTracks = await invoke('get_tracks_by_ids', { + const qobuzTracks = await invoke('v2_get_tracks_batch', { trackIds: pending.trackIds }); tracks = qobuzTracks.map((track, idx) => mapPlaylistTrack(track, idx)); @@ -619,9 +619,9 @@ // === Regular playlist — decide strategy based on track count === // Phase 1: lightweight metadata + track IDs (always fast) - console.log(`[Perf] invoke get_playlist_track_ids START (+${(performance.now() - _t0).toFixed(1)}ms)`); - const meta = await invoke('get_playlist_track_ids', { playlistId }); - console.log(`[Perf] invoke get_playlist_track_ids DONE (+${(performance.now() - _t0).toFixed(1)}ms) — ${meta.track_ids.length} IDs`); + console.log(`[Perf] invoke v2_get_playlist_track_ids START (+${(performance.now() - _t0).toFixed(1)}ms)`); + const meta = await invoke('v2_get_playlist_track_ids', { playlistId }); + console.log(`[Perf] invoke v2_get_playlist_track_ids DONE (+${(performance.now() - _t0).toFixed(1)}ms) — ${meta.track_ids.length} IDs`); // Set playlist metadata immediately playlist = { diff --git a/src/lib/components/views/SearchView.svelte b/src/lib/components/views/SearchView.svelte index 76e16361b..28a5c6daa 100644 --- a/src/lib/components/views/SearchView.svelte +++ b/src/lib/components/views/SearchView.svelte @@ -492,7 +492,7 @@ // Search based on active tab - reset to first page if (activeTab === 'all') { // Use title case for better most_popular results from Qobuz API - const results = await invoke>('search_all', { + const results = await invoke>('v2_search_all', { query: toTitleCase(searchQuery) }); // Only apply if query hasn't changed while we were fetching diff --git a/src/lib/components/views/SettingsView.svelte b/src/lib/components/views/SettingsView.svelte index 27ed73dac..3b5768e06 100644 --- a/src/lib/components/views/SettingsView.svelte +++ b/src/lib/components/views/SettingsView.svelte @@ -1000,7 +1000,7 @@ async function loadLastfmState() { try { // Check if embedded (build-time) credentials are available - hasEmbeddedCredentials = await invoke('lastfm_has_embedded_credentials'); + hasEmbeddedCredentials = await invoke('v2_lastfm_has_embedded_credentials'); // Load saved credentials from localStorage (for user-provided keys) const savedApiKey = getUserItem('qbz-lastfm-api-key'); @@ -1647,7 +1647,7 @@ async function loadRemoteControlStatus() { try { - const status = await invoke('remote_control_get_status'); + const status = await invoke('v2_remote_control_get_status'); remoteControlStatus = status; remoteControlEnabled = status.enabled; remoteControlPort = status.port; @@ -1663,7 +1663,7 @@ async function handleRemoteControlToggle(enabled: boolean) { remoteControlLoading = true; try { - const status = await invoke('remote_control_set_enabled', { enabled }); + const status = await invoke('v2_remote_control_set_enabled', { enabled }); remoteControlStatus = status; remoteControlEnabled = status.enabled; remoteControlPort = status.port; @@ -1685,7 +1685,7 @@ if (!Number.isFinite(value)) return; remoteControlLoading = true; try { - const status = await invoke('remote_control_set_port', { port: value }); + const status = await invoke('v2_remote_control_set_port', { port: value }); remoteControlStatus = status; remoteControlEnabled = status.enabled; remoteControlPort = status.port; @@ -1710,7 +1710,7 @@ } remoteControlLoading = true; try { - const qr = await invoke('remote_control_get_pairing_qr'); + const qr = await invoke('v2_remote_control_get_pairing_qr'); remoteControlQrData = qr.qrDataUrl; remoteControlUrl = qr.url; remoteControlQrOpen = true; @@ -1736,11 +1736,11 @@ remoteControlLoading = true; try { - const qr = await invoke('remote_control_regenerate_token'); + const qr = await invoke('v2_remote_control_regenerate_token'); remoteControlQrData = qr.qrDataUrl; remoteControlUrl = qr.url; remoteControlQrOpen = true; - const status = await invoke('remote_control_get_status'); + const status = await invoke('v2_remote_control_get_status'); remoteControlStatus = status; remoteControlEnabled = status.enabled; remoteControlPort = status.port; @@ -1777,7 +1777,7 @@ async function handleRemoteControlSecureChange(secure: boolean) { remoteControlLoading = true; try { - const status = await invoke('remote_control_set_secure', { secure }); + const status = await invoke('v2_remote_control_set_secure', { secure }); remoteControlStatus = status; remoteControlEnabled = status.enabled; remoteControlPort = status.port; @@ -2068,7 +2068,7 @@ async function loadFlatpakStatus() { try { - isFlatpak = await invoke('is_running_in_flatpak'); + isFlatpak = await invoke('v2_is_running_in_flatpak'); if (isFlatpak) { flatpakHelpText = await invoke('get_flatpak_help_text'); } @@ -2378,7 +2378,7 @@ async function loadDeviceSampleRateLimit(deviceId: string) { try { - const rate = await invoke('get_device_sample_rate_limit', { deviceId }); + const rate = await invoke('v2_get_device_sample_rate_limit', { deviceId }); deviceMaxSampleRate = rate; console.log('[Audio] Loaded sample rate limit for', deviceId, ':', rate); } catch (err) { @@ -2453,7 +2453,7 @@ async function loadLyricsCacheStats() { try { - const stats = await invoke<{ entries: number; sizeBytes: number }>('lyrics_get_cache_stats'); + const stats = await invoke<{ entries: number; sizeBytes: number }>('v2_lyrics_get_cache_stats'); lyricsCacheStats = stats; } catch (err) { console.error('Failed to load lyrics cache stats:', err); @@ -2471,7 +2471,7 @@ async function loadDownloadSettings() { try { - const settings = await invoke<{download_root: string, show_in_library: boolean}>('get_download_settings'); + const settings = await invoke<{download_root: string, show_in_library: boolean}>('v2_get_download_settings'); showQobuzDownloadsInLibrary = settings.show_in_library; } catch (err) { console.error('Failed to load download settings:', err); @@ -2566,7 +2566,7 @@ async function checkLegacyCachedFiles() { try { - const result = await invoke<{has_legacy_files: boolean, total_tracks: number}>('detect_legacy_cached_files'); + const result = await invoke<{has_legacy_files: boolean, total_tracks: number}>('v2_detect_legacy_cached_files'); if (result.has_legacy_files && result.total_tracks > 0) { legacyTracksCount = result.total_tracks; showMigrationModal = true; diff --git a/src/lib/services/recoService.ts b/src/lib/services/recoService.ts index d324e3007..fd19eec60 100644 --- a/src/lib/services/recoService.ts +++ b/src/lib/services/recoService.ts @@ -58,7 +58,7 @@ export async function getHomeSeeds(limits?: { topArtists?: number; favorites?: number; }): Promise { - return invoke('reco_get_home', { + return invoke('v2_reco_get_home', { limitRecentAlbums: limits?.recentAlbums, limitContinueTracks: limits?.continueTracks, limitTopArtists: limits?.topArtists, @@ -86,7 +86,7 @@ export async function getHomeSeedsML(limits?: { topArtists?: number; favorites?: number; }): Promise { - return invoke('reco_get_home_ml', { + return invoke('v2_reco_get_home_ml', { limitRecentAlbums: limits?.recentAlbums, limitContinueTracks: limits?.continueTracks, limitTopArtists: limits?.topArtists, diff --git a/src/lib/services/thumbnailService.ts b/src/lib/services/thumbnailService.ts index 26217ca12..9a3ff6bdc 100644 --- a/src/lib/services/thumbnailService.ts +++ b/src/lib/services/thumbnailService.ts @@ -25,7 +25,7 @@ export async function getThumbnailUrl(artworkPath: string): Promise { const requestPromise = (async () => { try { // Call backend to get/generate thumbnail - const thumbnailPath = await invoke('library_get_thumbnail', { + const thumbnailPath = await invoke('v2_library_get_thumbnail', { artworkPath, }); @@ -87,7 +87,7 @@ export async function clearAllThumbnails(): Promise { * Get the size of the thumbnails cache in bytes. */ export async function getThumbnailsCacheSize(): Promise { - return invoke('library_get_thumbnails_cache_size'); + return invoke('v2_library_get_thumbnails_cache_size'); } /** diff --git a/src/lib/services/updatesService.ts b/src/lib/services/updatesService.ts index f1d1c4c4d..0784c92f5 100644 --- a/src/lib/services/updatesService.ts +++ b/src/lib/services/updatesService.ts @@ -140,7 +140,7 @@ export interface LaunchUpdateDecision { */ async function shouldShowFlatpakWelcome(): Promise { try { - const isFlatpak = await invoke('is_running_in_flatpak'); + const isFlatpak = await invoke('v2_is_running_in_flatpak'); if (!isFlatpak) return false; const alreadyShown = await invoke('has_flatpak_welcome_been_shown'); return !alreadyShown; diff --git a/src/lib/stores/offlineCacheState.ts b/src/lib/stores/offlineCacheState.ts index 5d225038a..1f8b5811a 100644 --- a/src/lib/stores/offlineCacheState.ts +++ b/src/lib/stores/offlineCacheState.ts @@ -81,7 +81,7 @@ function notifyListeners(): void { // Initialize offline cache states from backend export async function initOfflineCacheStates(): Promise { try { - const tracks = await invoke('get_cached_tracks'); + const tracks = await invoke('v2_get_cached_tracks'); for (const track of tracks) { offlineCacheStates.set(track.trackId, { status: track.status, @@ -203,7 +203,7 @@ export async function removeCachedTrack(trackId: number): Promise { // Get offline cache stats export async function getOfflineCacheStats(): Promise { - return invoke('get_offline_cache_stats'); + return invoke('v2_get_offline_cache_stats'); } // Clear all offline cache diff --git a/src/lib/stores/offlineStore.ts b/src/lib/stores/offlineStore.ts index af458b809..b3e5becc6 100644 --- a/src/lib/stores/offlineStore.ts +++ b/src/lib/stores/offlineStore.ts @@ -115,7 +115,7 @@ export function getOfflineReason(): OfflineReason | null { async function fetchStatus(): Promise { try { const wasOffline = status.isOffline; - const newStatus = await invoke('get_offline_status'); + const newStatus = await invoke('v2_get_offline_status'); const changed = JSON.stringify(status) !== JSON.stringify(newStatus); status = newStatus; if (changed) { @@ -149,7 +149,7 @@ async function fetchStatus(): Promise { */ async function fetchSettings(): Promise { try { - settings = await invoke('get_offline_settings'); + settings = await invoke('v2_get_offline_settings'); notifyListeners(); } catch (error) { const msg = String(error); @@ -166,7 +166,7 @@ async function fetchSettings(): Promise { */ export async function checkNetwork(): Promise { try { - return await invoke('check_network'); + return await invoke('v2_check_network'); } catch (error) { console.error('Failed to check network:', error); return false; @@ -184,7 +184,7 @@ export async function setManualOffline(enabled: boolean): Promise { console.log('[Offline] wasOffline:', wasOffline); console.log('[Offline] ========================================'); - const newStatus = await invoke('set_manual_offline', { enabled }); + const newStatus = await invoke('v2_set_manual_offline', { enabled }); status = newStatus; settings.manualOfflineMode = enabled; diff --git a/src/lib/stores/playlistFoldersStore.ts b/src/lib/stores/playlistFoldersStore.ts index 1b18d90aa..dc2d5ce37 100644 --- a/src/lib/stores/playlistFoldersStore.ts +++ b/src/lib/stores/playlistFoldersStore.ts @@ -139,7 +139,7 @@ export async function loadFolders(): Promise { notifyListeners(); try { - const result = await invoke('get_playlist_folders'); + const result = await invoke('v2_get_playlist_folders'); folders = result; lastError = null; } catch (err) { @@ -161,7 +161,7 @@ export async function createFolder( iconColor?: string ): Promise { try { - const folder = await invoke('create_playlist_folder', { + const folder = await invoke('v2_create_playlist_folder', { name, iconType, iconPreset, @@ -193,7 +193,7 @@ export async function updateFolder( } ): Promise { try { - const folder = await invoke('update_playlist_folder', { + const folder = await invoke('v2_update_playlist_folder', { id, name: updates.name, iconType: updates.iconType, diff --git a/src/lib/stores/updatesStore.ts b/src/lib/stores/updatesStore.ts index 6b58c7c0c..68be33572 100644 --- a/src/lib/stores/updatesStore.ts +++ b/src/lib/stores/updatesStore.ts @@ -67,7 +67,7 @@ export async function initUpdatesStore(): Promise { // Only fetch once. if (!versionLoaded) { try { - currentVersion = await invoke('get_current_version'); + currentVersion = await invoke('v2_get_current_version'); versionLoaded = true; } catch (error) { console.error('[Updates] Failed to get current version:', error); @@ -79,7 +79,7 @@ export async function initUpdatesStore(): Promise { // so a pre-login failure doesn't lock in stale defaults. if (!prefsLoaded) { try { - preferences = await invoke('get_update_preferences'); + preferences = await invoke('v2_get_update_preferences'); prefsLoaded = true; } catch { // Expected before login — store not initialised yet. @@ -91,7 +91,7 @@ export async function initUpdatesStore(): Promise { export async function refreshUpdatePreferences(): Promise { try { - preferences = await invoke('get_update_preferences'); + preferences = await invoke('v2_get_update_preferences'); notify(); } catch (error) { console.debug('[Updates] Failed to refresh update preferences:', error); @@ -121,7 +121,7 @@ export async function setShowWhatsNewOnLaunch(enabled: boolean): Promise { } export async function checkForUpdates(mode: 'launch' | 'manual'): Promise { - const result = await invoke('check_for_updates', { mode }); + const result = await invoke('v2_check_for_updates', { mode }); return { ...result, release: result.release ?? null, @@ -130,7 +130,7 @@ export async function checkForUpdates(mode: 'launch' | 'manual'): Promise { try { - const release = await invoke('fetch_release_for_version', { version }); + const release = await invoke('v2_fetch_release_for_version', { version }); return release ?? null; } catch (error) { console.debug('[Updates] Failed to fetch release for version:', version, error); @@ -148,7 +148,7 @@ export async function ignoreRelease(version: string): Promise { export async function hasWhatsNewBeenShown(version: string): Promise { try { - return await invoke('has_whats_new_been_shown', { version }); + return await invoke('v2_has_whats_new_been_shown', { version }); } catch (error) { console.debug('[Updates] Failed to check whats_new_shown:', version, error); return false; @@ -162,4 +162,3 @@ export async function markWhatsNewShown(version: string): Promise { console.debug('[Updates] Failed to mark whats_new_shown:', version, error); } } - diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 05133e465..fccf6b7fc 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -751,7 +751,7 @@ async function handleMusicianClick(name: string, role: string) { showToast($t('toast.lookingUp', { values: { name } }), 'info'); try { - const musician = await invoke('resolve_musician', { name, role }); + const musician = await invoke('v2_resolve_musician', { name, role }); console.log('Resolved musician:', musician); switch (musician.confidence) {