Merge refactor/internal/v2-cleanroom-final into pre-release

This commit is contained in:
vicrodh committed 2026-04-26 13:07:17 -06:00
commit fd545eccaa
18 files changed
+533 -27

No files matched your search

+2
View File
@@ -44,6 +44,7 @@ pub mod dynamic_amplify;
pub mod loudness;
pub mod loudness_analyzer;
pub mod loudness_cache;
pub mod output_sinks;
pub mod settings;
pub mod visualizer;
@@ -65,6 +66,7 @@ pub use dynamic_amplify::DynamicAmplify;
pub use loudness::{calculate_gain_factor, db_to_linear, extract_replaygain, ReplayGainData};
pub use loudness_analyzer::LoudnessAnalyzer;
pub use loudness_cache::LoudnessCache;
pub use output_sinks::{list_output_sinks, OutputSinkInfo};
pub use settings::AudioSettings;
pub use visualizer::{RingBuffer, TappedSource, VisualizerTap};
+170
View File
@@ -0,0 +1,170 @@
//! Output sink enumeration (frontend-shaped diagnostic).
//!
//! Provides a small, frontend-facing struct (`OutputSinkInfo`) listing the
//! available CPAL output devices. This is the same data the legacy
//! `get_pipewire_sinks` command exposed — the simpler shape (`name`,
//! `description`, `volume`, `is_default`) used by the audio settings UI
//! and the AudioOutputBadges component.
//!
//! NOTE: This is the same CPAL host the Player itself opens, so the
//! `name` returned here is guaranteed to be a valid identifier the
//! audio backend can re-open later. It is intentionally NOT the richer
//! `AudioDevice` struct from `backend::AudioBackend::enumerate_devices`,
//! which carries sample-rate probing data the Settings UI does not need.
use serde::Serialize;
/// Frontend-shaped info for a single audio output device.
///
/// Mirrors the legacy `PipewireSink` struct so the existing TypeScript
/// `PipewireSink` interface can consume V2 output unchanged.
#[derive(Debug, Clone, Serialize)]
pub struct OutputSinkInfo {
/// Internal name (e.g. CPAL device name; on Linux this is the
/// PipeWire/PulseAudio sink name like `alsa_output.usb-XXX`).
pub name: String,
/// User-friendly description. On PipeWire the CPAL name is already
/// user-readable; on macOS/Windows the name itself is descriptive.
pub description: String,
/// Current volume percentage (0–100). CPAL does not expose this so
/// it is always `None` here; preserved for API compatibility.
pub volume: Option<u32>,
/// Whether this is the default sink.
pub is_default: bool,
}
/// Resolve the CPAL `description().name()` for a device, returning `None`
/// if the description cannot be queried.
fn cpal_device_name(device: &rodio::cpal::Device) -> Option<String> {
use rodio::cpal::traits::DeviceTrait;
device
.description()
.ok()
.map(|description| description.name().to_string())
}
/// Enumerate the system's CPAL output devices.
///
/// On Linux the CPAL host is PipeWire/PulseAudio (rodio defaults to
/// CPAL's PipeWire host on modern distros); on macOS/Windows it is the
/// platform default. Output is shaped for the audio settings UI.
///
/// CRITICAL: The returned `name` is exactly the CPAL device name, so it
/// matches what the audio backend uses to re-open the device later. Do
/// NOT substitute a friendlier description for `name`.
#[cfg(target_os = "linux")]
pub fn list_output_sinks() -> Result<Vec<OutputSinkInfo>, String> {
log::debug!("[qbz-audio] list_output_sinks (Linux, using CPAL)");
use rodio::cpal::traits::{DeviceTrait, HostTrait};
let host = rodio::cpal::default_host();
let default_device_name = host
.default_output_device()
.and_then(|d| cpal_device_name(&d));
log::debug!(
"[qbz-audio] CPAL default device: {:?}",
default_device_name
);
let sinks: Vec<OutputSinkInfo> = host
.output_devices()
.map_err(|e| format!("Failed to enumerate devices: {}", e))?
.enumerate()
.filter_map(|(idx, device)| {
let name = match cpal_device_name(&device) {
Some(name) => name,
None => {
log::warn!("[qbz-audio] [{}] Failed to get device description", idx);
return None;
}
};
let is_default = default_device_name
.as_ref()
.map(|d| d == &name)
.unwrap_or(false);
// Same diagnostic logging as the legacy command, so log output
// for the V2 command matches what users / support reports
// already document.
let configs_info = device
.supported_output_configs()
.ok()
.map(|configs| {
let config_strs: Vec<String> = configs
.take(3)
.map(|c| format!("{}ch/{}Hz", c.channels(), c.max_sample_rate()))
.collect();
config_strs.join(", ")
})
.unwrap_or_else(|| "no configs".to_string());
log::debug!(
"[qbz-audio] [{}] Device: '{}' (default: {}) - Configs: {}",
idx,
name,
is_default,
configs_info
);
// Use the CPAL name for both `name` and `description`: PipeWire
// CPAL names are already user-friendly, and storing the same
// value as `name` guarantees the saved id reopens correctly.
Some(OutputSinkInfo {
name: name.clone(),
description: name,
volume: None,
is_default,
})
})
.collect();
log::debug!(
"[qbz-audio] Found {} audio output devices via CPAL",
sinks.len()
);
Ok(sinks)
}
/// Enumerate the system's CPAL output devices (macOS/Windows).
///
/// CPAL device names on these platforms are already descriptive enough
/// to display directly to the user.
#[cfg(not(target_os = "linux"))]
pub fn list_output_sinks() -> Result<Vec<OutputSinkInfo>, String> {
log::info!("[qbz-audio] list_output_sinks (non-Linux, using CPAL)");
use rodio::cpal::traits::HostTrait;
let host = rodio::cpal::default_host();
let default_device_name = host
.default_output_device()
.and_then(|d| cpal_device_name(&d));
let sinks: Vec<OutputSinkInfo> = host
.output_devices()
.map_err(|e| format!("Failed to enumerate devices: {}", e))?
.filter_map(|device| {
cpal_device_name(&device).map(|name| {
let is_default = default_device_name
.as_ref()
.map(|d| d == &name)
.unwrap_or(false);
OutputSinkInfo {
name: name.clone(),
description: name,
volume: None,
is_default,
}
})
})
.collect();
log::info!("[qbz-audio] Found {} audio output devices", sinks.len());
Ok(sinks)
}
+82 -3
View File
@@ -7,11 +7,11 @@ use std::sync::Arc;
use tokio::sync::RwLock;
use qbz_models::{
Album, Artist, CoreEvent, DiscoverAlbum, DiscoverData, DiscoverPlaylistsResponse,
Album, Artist, ArtistAlbums, CoreEvent, DiscoverAlbum, DiscoverData, DiscoverPlaylistsResponse,
DiscoverResponse, FrontendAdapter, GenreInfo, LabelExploreResponse, LabelGetListResponse,
LabelListPage, LabelPageData, LabelStoryResponse, PageArtistResponse,
Playlist, PlaylistTag, Quality, QueueState, QueueTrack, RepeatMode, SearchResultsPage,
StreamUrl, Track, UserSession,
Playlist, PlaylistTag, Quality, QueueState, QueueTrack, ReleasesGridResponse, RepeatMode,
SearchResultsPage, StreamUrl, Track, TracksContainer, UserSession,
};
use qbz_player::{PlaybackState, Player, QueueManager};
use qbz_qobuz::QobuzClient;
@@ -841,6 +841,85 @@ impl<A: FrontendAdapter + Send + Sync + 'static> QbzCore<A> {
.map_err(CoreError::Api)
}
/// Get an artist's albums collection (paginated `ArtistAlbums` only).
///
/// Equivalent to `get_artist_with_albums` but projects only the `albums`
/// field for callers that don't need the full artist envelope.
pub async fn get_artist_albums(
&self,
artist_id: u64,
limit: Option<u32>,
offset: Option<u32>,
) -> Result<ArtistAlbums, CoreError> {
let client = self.client.read().await;
let client = client.as_ref().ok_or(CoreError::NotInitialized)?;
let artist = client
.get_artist_with_pagination(artist_id, true, limit, offset)
.await
.map_err(CoreError::Api)?;
artist
.albums
.ok_or_else(|| CoreError::Api(qbz_qobuz::ApiError::ApiResponse(
"No albums in artist response".to_string(),
)))
}
/// Get artist detail with albums, playlists and appears-on tracks.
///
/// Backs the suggestions panel: requests `extra=albums,tracks_appears_on,playlists`
/// from `/artist/get` so callers can read `playlists` and `tracks_appears_on`
/// without a second round-trip.
pub async fn get_artist_detail(
&self,
artist_id: u64,
limit: Option<u32>,
offset: Option<u32>,
) -> Result<Artist, CoreError> {
let client = self.client.read().await;
let client = client.as_ref().ok_or(CoreError::NotInitialized)?;
client
.get_artist_detail(artist_id, limit, offset)
.await
.map_err(CoreError::Api)
}
/// Get an artist's popular/top tracks (`/artist/get?extra=tracks`).
pub async fn get_artist_tracks(
&self,
artist_id: u64,
limit: u32,
offset: u32,
) -> Result<TracksContainer, CoreError> {
let client = self.client.read().await;
let client = client.as_ref().ok_or(CoreError::NotInitialized)?;
client
.get_artist_tracks(artist_id, limit, offset)
.await
.map_err(CoreError::Api)
}
/// Get an artist's releases grid (paginated by `release_type`).
pub async fn get_releases_grid(
&self,
artist_id: u64,
release_type: &str,
limit: u32,
offset: u32,
sort: Option<&str>,
) -> Result<ReleasesGridResponse, CoreError> {
let client = self.client.read().await;
let client = client.as_ref().ok_or(CoreError::NotInitialized)?;
client
.get_releases_grid(artist_id, release_type, limit, offset, sort)
.await
.map_err(CoreError::Api)
}
/// Get label page (aggregated: top tracks, releases, playlists, artists)
pub async fn get_label_page(&self, label_id: u64) -> Result<LabelPageData, CoreError> {
let client = self.client.read().await;
+37
View File
@@ -18,6 +18,43 @@ pub enum AudioDevicePresence {
Inconclusive,
}
/// Snapshot of which output device is currently in use and whether we're playing.
/// Mirrors the legacy `AudioOutputStatus` shape so the frontend type is unchanged.
#[derive(Debug, serde::Serialize)]
pub struct V2AudioOutputStatus {
pub device_name: Option<String>,
pub is_playing: bool,
}
/// Get current audio output status (V2 — reads from CoreBridge.player state).
///
/// Returns the device name actually in use by the audio thread plus whether
/// playback is currently active. Used by AudioOutputBadges to render the
/// active sink + DAC indicators in the now-playing bar.
#[tauri::command]
pub async fn v2_get_audio_output_status(
bridge: State<'_, CoreBridgeState>,
) -> Result<V2AudioOutputStatus, RuntimeError> {
let bridge_guard = bridge.get().await;
let player = bridge_guard.player();
Ok(V2AudioOutputStatus {
device_name: player.state.current_device(),
is_playing: player.state.is_playing(),
})
}
/// List the available CPAL output sinks (V2).
///
/// Delegates enumeration to `qbz_audio::output_sinks::list_output_sinks`,
/// which returns the same `{name, description, volume, is_default}` shape
/// the legacy `get_pipewire_sinks` command returned. Used by the audio
/// settings UI and AudioOutputBadges to populate the device picker and
/// label the currently-routed sink.
#[tauri::command]
pub fn v2_get_pipewire_sinks() -> Result<Vec<qbz_audio::OutputSinkInfo>, RuntimeError> {
qbz_audio::list_output_sinks().map_err(RuntimeError::Internal)
}
/// Snapshot the presence of the user's currently-selected output
/// device. Frontend calls this on demand (e.g. when a
/// `audio:device-missing` toast button fires Retry).
+125 -4
View File
@@ -1,10 +1,10 @@
use tauri::State;
use qbz_models::{
Album, Artist, AwardPageData, DiscoverAlbum, DiscoverData, DiscoverPlaylistsResponse,
DiscoverResponse, GenreInfo, LabelExploreResponse, LabelGetListResponse, LabelListPage,
LabelPageData, LabelStoryResponse, PageArtistResponse, Playlist, PlaylistTag,
SearchResultsPage, Track,
Album, Artist, ArtistAlbums, AwardPageData, DiscoverAlbum, DiscoverData,
DiscoverPlaylistsResponse, DiscoverResponse, GenreInfo, LabelExploreResponse,
LabelGetListResponse, LabelListPage, LabelPageData, LabelStoryResponse, PageArtistResponse,
Playlist, PlaylistTag, ReleasesGridResponse, SearchResultsPage, Track, TracksContainer,
};
use crate::artist_blacklist::BlacklistState;
@@ -399,6 +399,127 @@ pub async fn v2_get_artist_with_albums(
.map_err(RuntimeError::Internal)
}
/// Get an artist's albums collection (V2 - returns only the `ArtistAlbums` envelope).
#[tauri::command]
#[allow(non_snake_case)]
pub async fn v2_get_artist_albums(
artistId: u64,
limit: Option<u32>,
offset: Option<u32>,
bridge: State<'_, CoreBridgeState>,
runtime: State<'_, RuntimeManagerState>,
) -> Result<ArtistAlbums, RuntimeError> {
runtime
.manager()
.check_requirements(CommandRequirement::RequiresCoreBridgeAuth)
.await?;
log::info!(
"[V2] get_artist_albums: {} limit={:?} offset={:?}",
artistId,
limit,
offset
);
let bridge = bridge.get().await;
bridge
.get_artist_albums(artistId, limit, offset)
.await
.map_err(RuntimeError::Internal)
}
/// Get artist detail with albums, playlists and appears-on tracks (V2 - uses QbzCore).
#[tauri::command]
#[allow(non_snake_case)]
pub async fn v2_get_artist_detail(
artistId: u64,
limit: Option<u32>,
offset: Option<u32>,
bridge: State<'_, CoreBridgeState>,
runtime: State<'_, RuntimeManagerState>,
) -> Result<Artist, RuntimeError> {
runtime
.manager()
.check_requirements(CommandRequirement::RequiresCoreBridgeAuth)
.await?;
let limit = limit.or(Some(1000));
let offset = offset.or(Some(0));
log::info!(
"[V2] get_artist_detail: {} limit={:?} offset={:?}",
artistId,
limit,
offset
);
let bridge = bridge.get().await;
bridge
.get_artist_detail(artistId, limit, offset)
.await
.map_err(RuntimeError::Internal)
}
/// Get an artist's popular/top tracks (V2 - uses QbzCore).
#[tauri::command]
#[allow(non_snake_case)]
pub async fn v2_get_artist_tracks(
artistId: u64,
limit: Option<u32>,
offset: Option<u32>,
bridge: State<'_, CoreBridgeState>,
runtime: State<'_, RuntimeManagerState>,
) -> Result<TracksContainer, RuntimeError> {
runtime
.manager()
.check_requirements(CommandRequirement::RequiresCoreBridgeAuth)
.await?;
let limit = limit.unwrap_or(50);
let offset = offset.unwrap_or(0);
log::info!(
"[V2] get_artist_tracks: {} limit={} offset={}",
artistId,
limit,
offset
);
let bridge = bridge.get().await;
bridge
.get_artist_tracks(artistId, limit, offset)
.await
.map_err(RuntimeError::Internal)
}
/// Get an artist's releases grid paginated by release_type (V2 - uses QbzCore).
#[tauri::command]
#[allow(non_snake_case)]
pub async fn v2_get_releases_grid(
artistId: u64,
releaseType: String,
limit: Option<u32>,
offset: Option<u32>,
sort: Option<String>,
bridge: State<'_, CoreBridgeState>,
runtime: State<'_, RuntimeManagerState>,
) -> Result<ReleasesGridResponse, RuntimeError> {
runtime
.manager()
.check_requirements(CommandRequirement::RequiresCoreBridgeAuth)
.await?;
let limit = limit.unwrap_or(25);
let offset = offset.unwrap_or(0);
log::info!(
"[V2] get_releases_grid: {} type={} limit={} offset={}",
artistId,
releaseType,
limit,
offset
);
let bridge = bridge.get().await;
bridge
.get_releases_grid(artistId, &releaseType, limit, offset, sort.as_deref())
.await
.map_err(RuntimeError::Internal)
}
/// Get label page (aggregated: top tracks, releases, playlists, artists)
#[tauri::command]
#[allow(non_snake_case)]
@@ -1299,6 +1299,13 @@ pub fn v2_set_qobuz_tos_accepted(
crate::config::legal_settings::set_qobuz_tos_accepted(state, accepted)
}
#[tauri::command]
pub fn v2_get_qobuz_tos_accepted(
state: State<'_, crate::config::legal_settings::LegalSettingsState>,
) -> Result<bool, String> {
crate::config::legal_settings::get_qobuz_tos_accepted(state)
}
#[tauri::command]
pub fn v2_set_update_check_on_launch(
enabled: bool,
@@ -1382,6 +1389,13 @@ pub fn v2_mark_flatpak_welcome_shown(
crate::updates::mark_flatpak_welcome_shown(state)
}
#[tauri::command]
pub fn v2_has_flatpak_welcome_been_shown(
state: State<'_, crate::updates::UpdatesState>,
) -> Result<bool, String> {
crate::updates::has_flatpak_welcome_been_shown(state)
}
#[tauri::command]
pub fn v2_get_backend_logs() -> Vec<String> {
crate::logging::get_backend_logs()
@@ -1469,6 +1483,16 @@ pub fn v2_is_running_in_flatpak() -> bool {
crate::flatpak::is_running_in_flatpak()
}
#[tauri::command]
pub fn v2_get_flatpak_help_text() -> String {
crate::flatpak::get_flatpak_guidance()
}
#[tauri::command]
pub fn v2_detect_desktop_theme() -> crate::desktop_theme::DesktopThemeInfo {
crate::desktop_theme::detect_desktop_theme()
}
#[tauri::command]
pub fn v2_is_auto_update_eligible() -> bool {
crate::updates::is_auto_update_eligible()
@@ -1486,6 +1510,13 @@ pub fn v2_mark_snap_welcome_shown(
crate::updates::mark_snap_welcome_shown(state)
}
#[tauri::command]
pub fn v2_has_snap_welcome_been_shown(
state: State<'_, crate::updates::UpdatesState>,
) -> Result<bool, String> {
crate::updates::has_snap_welcome_been_shown(state)
}
#[tauri::command]
pub async fn v2_detect_legacy_cached_files(
cache_state: State<'_, OfflineCacheState>,
+59 -4
View File
@@ -13,10 +13,11 @@ use tokio::sync::RwLock;
use qbz_audio::{settings::AudioSettingsStore, AudioDiagnostic, AudioSettings, VisualizerTap};
use qbz_core::QbzCore;
use qbz_models::{
Album, Artist, DiscoverAlbum, DiscoverData, DiscoverPlaylistsResponse, DiscoverResponse,
GenreInfo, LabelExploreResponse, LabelGetListResponse, LabelListPage, LabelPageData,
LabelStoryResponse, PageArtistResponse, Playlist, PlaylistTag, Quality,
QueueState, QueueTrack, RepeatMode, SearchResultsPage, StreamUrl, Track, UserSession,
Album, Artist, ArtistAlbums, DiscoverAlbum, DiscoverData, DiscoverPlaylistsResponse,
DiscoverResponse, GenreInfo, LabelExploreResponse, LabelGetListResponse, LabelListPage,
LabelPageData, LabelStoryResponse, PageArtistResponse, Playlist, PlaylistTag, Quality,
QueueState, QueueTrack, ReleasesGridResponse, RepeatMode, SearchResultsPage, StreamUrl, Track,
TracksContainer, UserSession,
};
use qbz_player::{PlaybackState, Player};
@@ -555,6 +556,60 @@ impl CoreBridge {
.map_err(|e| e.to_string())
}
/// Get an artist's albums collection (paginated `ArtistAlbums` only).
pub async fn get_artist_albums(
&self,
artist_id: u64,
limit: Option<u32>,
offset: Option<u32>,
) -> Result<ArtistAlbums, String> {
self.core
.get_artist_albums(artist_id, limit, offset)
.await
.map_err(|e| e.to_string())
}
/// Get artist detail (albums, playlists, appears-on tracks).
pub async fn get_artist_detail(
&self,
artist_id: u64,
limit: Option<u32>,
offset: Option<u32>,
) -> Result<Artist, String> {
self.core
.get_artist_detail(artist_id, limit, offset)
.await
.map_err(|e| e.to_string())
}
/// Get an artist's popular/top tracks.
pub async fn get_artist_tracks(
&self,
artist_id: u64,
limit: u32,
offset: u32,
) -> Result<TracksContainer, String> {
self.core
.get_artist_tracks(artist_id, limit, offset)
.await
.map_err(|e| e.to_string())
}
/// Get an artist's releases grid (paginated by `release_type`).
pub async fn get_releases_grid(
&self,
artist_id: u64,
release_type: &str,
limit: u32,
offset: u32,
sort: Option<&str>,
) -> Result<ReleasesGridResponse, String> {
self.core
.get_releases_grid(artist_id, release_type, limit, offset, sort)
.await
.map_err(|e| e.to_string())
}
/// Get label page (aggregated: top tracks, releases, playlists, artists)
pub async fn get_label_page(&self, label_id: u64) -> Result<LabelPageData, String> {
self.core
+11
View File
@@ -1398,6 +1398,7 @@ pub fn run() {
commands::playback::get_pipewire_sinks,
commands::playback::get_audio_output_status,
flatpak::get_flatpak_help_text,
commands_v2::v2_get_flatpak_help_text,
config::legal_settings::get_qobuz_tos_accepted,
updates::has_flatpak_welcome_been_shown,
updates::has_snap_welcome_been_shown,
@@ -1579,6 +1580,7 @@ pub fn run() {
commands_v2::v2_is_running_in_flatpak,
commands_v2::v2_is_running_in_snap,
commands_v2::v2_mark_snap_welcome_shown,
commands_v2::v2_has_snap_welcome_been_shown,
commands_v2::v2_detect_legacy_cached_files,
commands_v2::v2_reco_log_event,
commands_v2::v2_reco_train_scores,
@@ -1625,6 +1627,7 @@ 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_qobuz_tos_accepted,
commands_v2::v2_get_update_preferences,
commands_v2::v2_get_current_version,
commands_v2::v2_check_for_updates,
@@ -1636,6 +1639,7 @@ pub fn run() {
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_has_flatpak_welcome_been_shown,
commands_v2::v2_is_auto_update_eligible,
commands_v2::v2_get_backend_logs,
commands_v2::v2_upload_logs_to_paste,
@@ -1699,6 +1703,10 @@ pub fn run() {
commands_v2::v2_get_artist_page,
commands_v2::v2_get_similar_artists,
commands_v2::v2_get_artist_with_albums,
commands_v2::v2_get_artist_albums,
commands_v2::v2_get_artist_detail,
commands_v2::v2_get_artist_tracks,
commands_v2::v2_get_releases_grid,
commands_v2::v2_get_label_page,
commands_v2::v2_get_label_explore,
commands_v2::v2_get_label_albums,
@@ -1723,6 +1731,8 @@ pub fn run() {
commands_v2::v2_prefetch_track,
commands_v2::v2_reinit_audio_device,
commands_v2::v2_check_audio_device_presence,
commands_v2::v2_get_audio_output_status,
commands_v2::v2_get_pipewire_sinks,
commands_v2::v2_get_audio_settings,
commands_v2::v2_set_audio_output_device,
commands_v2::v2_set_audio_exclusive_mode,
@@ -1868,6 +1878,7 @@ pub fn run() {
commands_v2::v2_clear_image_cache,
// Desktop theme detection (KDE/Klassy → adaptive window controls)
desktop_theme::detect_desktop_theme,
commands_v2::v2_detect_desktop_theme,
// Mixtapes & Collections
commands_v2::v2_list_mixtape_collections,
commands_v2::v2_get_mixtape_collection,
+3 -3
View File
@@ -150,8 +150,8 @@
try {
const [settingsResult, statusResult, sinksResult, alsaResult, hwStatus] = await Promise.all([
invoke<AudioSettings>('v2_get_audio_settings'),
invoke<AudioOutputStatus>('get_audio_output_status'),
invoke<PipewireSink[]>('get_pipewire_sinks').catch(() => [] as PipewireSink[]),
invoke<AudioOutputStatus>('v2_get_audio_output_status'),
invoke<PipewireSink[]>('v2_get_pipewire_sinks').catch(() => [] as PipewireSink[]),
invoke<BackendAudioDevice[]>('v2_get_devices_for_backend', { backendType: 'Alsa' }).catch(() => [] as BackendAudioDevice[]),
invoke<HardwareAudioStatus>('v2_get_hardware_audio_status').catch(() => null)
]);
@@ -215,7 +215,7 @@
try {
const [hwStatus, status] = await Promise.all([
invoke<HardwareAudioStatus>('v2_get_hardware_audio_status').catch(() => null),
invoke<AudioOutputStatus>('get_audio_output_status').catch(() => null),
invoke<AudioOutputStatus>('v2_get_audio_output_status').catch(() => null),
]);
hardwareStatus = hwStatus;
if (status) outputStatus = status;
@@ -110,7 +110,7 @@
try {
// Get artist detail which includes playlists and tracks
const artist = await invoke<ArtistDetail>('get_artist_detail', { artistId: id });
const artist = await invoke<ArtistDetail>('v2_get_artist_detail', { artistId: id });
loadedArtistId = id;
// Extract curated playlists (max 2)
@@ -133,7 +133,7 @@
if (tracks.length < 5) {
console.log(`[Suggestions] Sparse tracks_appears_on (${tracks.length}), fetching artist tracks...`);
try {
const artistTracks = await invoke<TracksContainer>('get_artist_tracks', {
const artistTracks = await invoke<TracksContainer>('v2_get_artist_tracks', {
artistId: id,
limit: 30,
offset: 0
@@ -342,7 +342,7 @@
albumsError = null;
try {
const result = await invoke<{ items: QobuzAlbum[] }>('get_artist_albums', {
const result = await invoke<{ items: QobuzAlbum[] }>('v2_get_artist_albums', {
artistId: artist.id,
limit: 500,
offset: 0,
@@ -938,7 +938,7 @@
artistAlbumsError = null;
try {
const result = await invoke<{ items: QobuzAlbum[]; total: number }>('get_artist_albums', {
const result = await invoke<{ items: QobuzAlbum[]; total: number }>('v2_get_artist_albums', {
artistId: artist.id,
limit: 500, // Fetch more to ensure we have enough Discography albums after filtering
offset: 0
+1 -1
View File
@@ -599,7 +599,7 @@
for (let i = 0; i < uncachedSeeds.length; i += BATCH_SIZE) {
const batch = uncachedSeeds.slice(i, i + BATCH_SIZE);
const results = await Promise.allSettled(
batch.map(seed => invoke<QobuzArtist>('get_artist_basic', { artistId: seed.artistId }))
batch.map(seed => invoke<QobuzArtist>('v2_get_artist', { artistId: seed.artistId }))
);
results.forEach((result, index) => {
+2 -2
View File
@@ -2716,7 +2716,7 @@
async function loadAudioDevices() {
try {
// Load PipeWire sinks - these have friendly descriptions already
const sinks = await invoke<PipewireSink[]>('get_pipewire_sinks').catch(() => [] as PipewireSink[]);
const sinks = await invoke<PipewireSink[]>('v2_get_pipewire_sinks').catch(() => [] as PipewireSink[]);
pipewireSinks = sinks;
// Load hardware audio status
@@ -2787,7 +2787,7 @@
try {
isFlatpak = await invoke<boolean>('v2_is_running_in_flatpak');
if (isFlatpak) {
flatpakHelpText = await invoke<string>('get_flatpak_help_text');
flatpakHelpText = await invoke<string>('v2_get_flatpak_help_text');
}
} catch (err) {
console.error('Failed to check Flatpak status:', err);
+2 -2
View File
@@ -158,7 +158,7 @@ async function shouldShowFlatpakWelcome(): Promise<boolean> {
try {
const isFlatpak = await invoke<boolean>('v2_is_running_in_flatpak');
if (!isFlatpak) return false;
const alreadyShown = await invoke<boolean>('has_flatpak_welcome_been_shown');
const alreadyShown = await invoke<boolean>('v2_has_flatpak_welcome_been_shown');
return !alreadyShown;
} catch {
return false;
@@ -183,7 +183,7 @@ async function shouldShowSnapWelcome(): Promise<boolean> {
try {
const isSnap = await invoke<boolean>('v2_is_running_in_snap');
if (!isSnap) return false;
const alreadyShown = await invoke<boolean>('has_snap_welcome_been_shown');
const alreadyShown = await invoke<boolean>('v2_has_snap_welcome_been_shown');
return !alreadyShown;
} catch {
return false;
+1 -1
View File
@@ -18,7 +18,7 @@ export async function loadTosAcceptance(): Promise<boolean> {
if (!browser) return false;
try {
const accepted = await invoke<boolean>('get_qobuz_tos_accepted');
const accepted = await invoke<boolean>('v2_get_qobuz_tos_accepted');
qobuzTosAccepted.set(accepted);
initialized = true;
return accepted;
+1 -1
View File
@@ -229,7 +229,7 @@ export async function detectDesktopThemeCached(force = false): Promise<DesktopTh
detectInFlight = (async () => {
try {
const { invoke } = await import('@tauri-apps/api/core');
const info = (await invoke('detect_desktop_theme')) as DesktopThemeInfo;
const info = (await invoke('v2_detect_desktop_theme')) as DesktopThemeInfo;
cachedTheme = info;
return info;
} catch (e) {
+2 -2
View File
@@ -1857,7 +1857,7 @@
g => g.type === 'ep' || g.type === 'single' || g.type === 'epSingle'
);
if (!hasEpGroup && selectedArtist.epsSingles.length === 0) {
invoke<ReleasesGridResponse>('get_releases_grid', {
invoke<ReleasesGridResponse>('v2_get_releases_grid', {
artistId, releaseType: 'epSingle', limit: 25, offset: 0
}).then(result => {
if (result.items.length > 0 && selectedArtist?.id === artistId) {
@@ -2128,7 +2128,7 @@
isArtistAlbumsLoading = true;
try {
const result = await invoke<ReleasesGridResponse>('get_releases_grid', {
const result = await invoke<ReleasesGridResponse>('v2_get_releases_grid', {
artistId: selectedArtist.id,
releaseType: apiReleaseType,
limit: 25,