diff --git a/src-tauri/src/cast/commands.rs b/src-tauri/src/cast/commands.rs deleted file mode 100644 index 72449db30..000000000 --- a/src-tauri/src/cast/commands.rs +++ /dev/null @@ -1,273 +0,0 @@ -//! Tauri commands for Chromecast casting - -use tauri::State; - -use crate::api::models::Quality; -use crate::cast::{ - CastError, CastPositionInfo, CastStatus, DiscoveredDevice, MediaMetadata, -}; -use crate::library::{AudioFormat, LibraryState}; -use crate::AppState; - -// CastState lives in `super::state`; re-export from commands so any -// historical `crate::cast::commands::CastState` paths continue to compile. -pub use super::state::CastState; - -// === Discovery === - -#[tauri::command] -pub async fn cast_start_discovery(state: State<'_, CastState>) -> Result<(), String> { - let mut discovery = state.discovery.lock().await; - discovery.start_discovery().map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn cast_stop_discovery(state: State<'_, CastState>) -> Result<(), String> { - let mut discovery = state.discovery.lock().await; - discovery.stop_discovery().map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn cast_get_devices( - state: State<'_, CastState>, -) -> Result, String> { - let discovery = state.discovery.lock().await; - Ok(discovery.get_discovered_devices()) -} - -// === Connection === - -#[tauri::command] -pub async fn cast_connect(device_id: String, state: State<'_, CastState>) -> Result<(), String> { - let device = { - let discovery = state.discovery.lock().await; - discovery - .get_device(&device_id) - .ok_or_else(|| CastError::DeviceNotFound(device_id.clone())) - .map_err(|e| e.to_string())? - }; - - state - .chromecast - .connect(device.ip.clone(), device.port) - .map_err(|e| e.to_string())?; - - { - let mut connected = state.connected_device_ip.lock().await; - *connected = Some(device.ip.clone()); - } - - Ok(()) -} - -#[tauri::command] -pub async fn cast_disconnect(state: State<'_, CastState>) -> Result<(), String> { - state.chromecast.disconnect().map_err(|e| e.to_string())?; - - { - let mut connected = state.connected_device_ip.lock().await; - *connected = None; - } - - Ok(()) -} - -#[tauri::command] -pub async fn cast_get_status(state: State<'_, CastState>) -> Result { - state.chromecast.get_status().map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn cast_get_position(state: State<'_, CastState>) -> Result { - state - .chromecast - .get_media_position() - .map_err(|e| e.to_string()) -} - -// === Playback === - -#[tauri::command] -pub async fn cast_play_track( - track_id: u64, - metadata: MediaMetadata, - state: State<'_, CastState>, - app_state: State<'_, AppState>, -) -> Result<(), String> { - let stream_url = { - let client = app_state.client.read().await; - client - .get_stream_url_with_fallback(track_id, Quality::HiRes) - .await - .map_err(|e| format!("Failed to get stream URL: {}", e))? - }; - - let content_type = stream_url.mime_type.clone(); - let cache = app_state.audio_cache.clone(); - - let audio_data = if let Some(cached) = cache.get(track_id) { - cached.data - } else { - let data = download_audio(&stream_url.url).await?; - cache.insert(track_id, data.clone()); - data - }; - - let target_ip = { - let connected = state.connected_device_ip.lock().await; - connected.clone() - }; - - // Ensure media server is started (lazy init) - state - .get_or_create_media_server() - .await - .map_err(|e| e.to_string())?; - - let url = { - let mut server_guard = state.media_server.lock().await; - let server = server_guard - .as_mut() - .ok_or("Media server not initialized")?; - server.register_audio(track_id, audio_data, &content_type); - let url = match target_ip.as_deref() { - Some(ip) => server.get_audio_url_for_target(track_id, ip), - None => server.get_audio_url(track_id), - }; - url.ok_or_else(|| "Failed to build media URL".to_string())? - }; - - state - .chromecast - .load_media(url, content_type, metadata) - .map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn cast_play_local_track( - track_id: i64, - state: State<'_, CastState>, - library_state: State<'_, LibraryState>, -) -> Result<(), String> { - let track = { - let db_opt__ = library_state.db.lock().await; - let db = db_opt__ - .as_ref() - .ok_or("No active session - please log in")?; - db.get_track(track_id) - .map_err(|e| e.to_string())? - .ok_or_else(|| "Track not found".to_string())? - }; - - let target_ip = { - let connected = state.connected_device_ip.lock().await; - connected.clone() - }; - - // Ensure media server is started (lazy init) - state - .get_or_create_media_server() - .await - .map_err(|e| e.to_string())?; - - let url = { - let mut server_guard = state.media_server.lock().await; - let server = server_guard - .as_mut() - .ok_or("Media server not initialized")?; - server - .register_file(track_id as u64, &track.file_path) - .map_err(|e| e.to_string())?; - let url = match target_ip.as_deref() { - Some(ip) => server.get_audio_url_for_target(track_id as u64, ip), - None => server.get_audio_url(track_id as u64), - }; - url.ok_or_else(|| "Failed to build media URL".to_string())? - }; - - let metadata = MediaMetadata { - title: track.title, - artist: track.artist, - album: track.album, - artwork_url: None, - duration_secs: Some(track.duration_secs), - }; - - let content_type = content_type_from_format(&track.format).to_string(); - - state - .chromecast - .load_media(url, content_type, metadata) - .map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn cast_play(state: State<'_, CastState>) -> Result<(), String> { - state.chromecast.play().map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn cast_pause(state: State<'_, CastState>) -> Result<(), String> { - state.chromecast.pause().map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn cast_stop(state: State<'_, CastState>) -> Result<(), String> { - state.chromecast.stop().map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn cast_seek(position_secs: f64, state: State<'_, CastState>) -> Result<(), String> { - state - .chromecast - .seek(position_secs) - .map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn cast_set_volume(volume: f32, state: State<'_, CastState>) -> Result<(), String> { - state - .chromecast - .set_volume(volume) - .map_err(|e| e.to_string()) -} - -async fn download_audio(url: &str) -> Result, String> { - use std::time::Duration; - - let client = reqwest::Client::builder() - .timeout(Duration::from_secs(120)) - .connect_timeout(Duration::from_secs(10)) - .build() - .map_err(|e| format!("Failed to create HTTP client: {}", e))?; - - let response = client - .get(url) - .header("User-Agent", "Mozilla/5.0") - .send() - .await - .map_err(|e| format!("Failed to fetch audio: {}", e))?; - - if !response.status().is_success() { - return Err(format!("HTTP error: {}", response.status())); - } - - let bytes = response - .bytes() - .await - .map_err(|e| format!("Failed to read audio bytes: {}", e))?; - - Ok(bytes.to_vec()) -} - -fn content_type_from_format(format: &AudioFormat) -> &'static str { - match format { - AudioFormat::Flac => "audio/flac", - AudioFormat::Alac => "audio/mp4", - AudioFormat::Wav => "audio/wav", - AudioFormat::Aiff => "audio/aiff", - AudioFormat::Ape => "audio/ape", - AudioFormat::Mp3 => "audio/mpeg", - AudioFormat::Unknown => "application/octet-stream", - } -} diff --git a/src-tauri/src/cast/dlna/commands.rs b/src-tauri/src/cast/dlna/commands.rs deleted file mode 100644 index ecb48699e..000000000 --- a/src-tauri/src/cast/dlna/commands.rs +++ /dev/null @@ -1,258 +0,0 @@ -//! Tauri commands for DLNA/UPnP casting - -use tauri::State; - -use crate::api::models::Quality; -use crate::cast::dlna::{ - DiscoveredDlnaDevice, DlnaConnection, DlnaError, DlnaMetadata, DlnaPositionInfo, DlnaStatus, -}; -use crate::AppState; - -// DlnaState lives in `super::state`; re-export from commands so any -// historical `crate::cast::dlna::commands::DlnaState` paths continue -// to compile. -pub use super::state::DlnaState; - -// === Discovery === - -#[tauri::command] -pub async fn dlna_start_discovery(state: State<'_, DlnaState>) -> Result<(), String> { - let mut discovery = state.discovery.lock().await; - discovery.start_discovery().await.map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn dlna_stop_discovery(state: State<'_, DlnaState>) -> Result<(), String> { - let mut discovery = state.discovery.lock().await; - discovery.stop_discovery().map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn dlna_get_devices( - state: State<'_, DlnaState>, -) -> Result, String> { - let discovery = state.discovery.lock().await; - Ok(discovery.get_discovered_devices()) -} - -// === Connection === - -#[tauri::command] -pub async fn dlna_connect(device_id: String, state: State<'_, DlnaState>) -> Result<(), String> { - let device = { - let discovery = state.discovery.lock().await; - discovery - .get_device(&device_id) - .ok_or_else(|| DlnaError::DeviceNotFound(device_id.clone())) - .map_err(|e| e.to_string())? - }; - - let connection = DlnaConnection::connect(device) - .await - .map_err(|e| e.to_string())?; - let mut state_connection = state.connection.lock().await; - *state_connection = Some(connection); - Ok(()) -} - -#[tauri::command] -pub async fn dlna_disconnect(state: State<'_, DlnaState>) -> Result<(), String> { - let mut connection = state.connection.lock().await; - if let Some(conn) = connection.as_mut() { - conn.disconnect().map_err(|e| e.to_string())?; - } - *connection = None; - Ok(()) -} - -#[tauri::command] -pub async fn dlna_get_status(state: State<'_, DlnaState>) -> Result { - let connection = state.connection.lock().await; - let conn = connection - .as_ref() - .ok_or_else(|| "Not connected".to_string())?; - Ok(conn.get_status()) -} - -/// Get current playback position from DLNA device -#[tauri::command] -pub async fn dlna_get_position(state: State<'_, DlnaState>) -> Result { - let connection = state.connection.lock().await; - let conn = connection - .as_ref() - .ok_or_else(|| "Not connected".to_string())?; - conn.get_position_info().await.map_err(|e| e.to_string()) -} - -// === Playback === - -/// Play a Qobuz track on the DLNA device -#[tauri::command] -pub async fn dlna_play_track( - track_id: u64, - metadata: DlnaMetadata, - dlna_state: State<'_, DlnaState>, - app_state: State<'_, AppState>, -) -> Result<(), String> { - log::info!("DLNA: dlna_play_track called for track_id={}", track_id); - - // Get stream URL from Qobuz - let stream_url = { - let client = app_state.client.read().await; - client - .get_stream_url_with_fallback(track_id, Quality::HiRes) - .await - .map_err(|e| format!("Failed to get stream URL: {}", e))? - }; - - let content_type = stream_url.mime_type.clone(); - let cache = app_state.audio_cache.clone(); - - // Download audio data - let audio_data = if let Some(cached) = cache.get(track_id) { - cached.data - } else { - let data = download_audio(&stream_url.url).await?; - cache.insert(track_id, data.clone()); - data - }; - - // Register with media server to get a URL the DLNA device can access - let target_ip = { - let connection = dlna_state.connection.lock().await; - connection.as_ref().map(|conn| conn.device_ip().to_string()) - }; - - // Ensure media server is started (lazy init) - dlna_state - .ensure_media_server() - .await - .map_err(|e| e.to_string())?; - - let url = { - let mut server_guard = dlna_state.media_server.lock().await; - let server = server_guard - .as_mut() - .ok_or("Media server not initialized")?; - server.register_audio(track_id, audio_data, &content_type); - let url = match target_ip.as_deref() { - Some(ip) => server.get_audio_url_for_target(track_id, ip), - None => server.get_audio_url(track_id), - }; - url.ok_or_else(|| "Failed to build media URL".to_string())? - }; - - log::info!( - "DLNA: Playing track {} via MediaServer URL: {}", - track_id, - url - ); - log::info!("DLNA: Content-Type from Qobuz: {}", content_type); - - // Load media on DLNA device - { - let mut connection = dlna_state.connection.lock().await; - let conn = connection - .as_mut() - .ok_or_else(|| "Not connected".to_string())?; - conn.load_media(&url, &metadata, &content_type) - .await - .map_err(|e| e.to_string())?; - } - - // Start playback - { - let mut connection = dlna_state.connection.lock().await; - let conn = connection - .as_mut() - .ok_or_else(|| "Not connected".to_string())?; - conn.play().await.map_err(|e| e.to_string())?; - } - - Ok(()) -} - -/// Load media without starting playback (for compatibility with existing castStore) -#[tauri::command] -pub async fn dlna_load_media( - _metadata: DlnaMetadata, - _state: State<'_, DlnaState>, -) -> Result<(), String> { - // This is a stub - actual loading happens in dlna_play_track - // The castStore calls this but we need the track_id for actual loading - log::warn!("dlna_load_media called without track_id - use dlna_play_track instead"); - Ok(()) -} - -#[tauri::command] -pub async fn dlna_play(state: State<'_, DlnaState>) -> Result<(), String> { - let mut connection = state.connection.lock().await; - let conn = connection - .as_mut() - .ok_or_else(|| "Not connected".to_string())?; - conn.play().await.map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn dlna_pause(state: State<'_, DlnaState>) -> Result<(), String> { - let mut connection = state.connection.lock().await; - let conn = connection - .as_mut() - .ok_or_else(|| "Not connected".to_string())?; - conn.pause().await.map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn dlna_stop(state: State<'_, DlnaState>) -> Result<(), String> { - let mut connection = state.connection.lock().await; - let conn = connection - .as_mut() - .ok_or_else(|| "Not connected".to_string())?; - conn.stop().await.map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn dlna_seek(position_secs: u64, state: State<'_, DlnaState>) -> Result<(), String> { - let mut connection = state.connection.lock().await; - let conn = connection - .as_mut() - .ok_or_else(|| "Not connected".to_string())?; - conn.seek(position_secs).await.map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn dlna_set_volume(volume: f32, state: State<'_, DlnaState>) -> Result<(), String> { - let mut connection = state.connection.lock().await; - let conn = connection - .as_mut() - .ok_or_else(|| "Not connected".to_string())?; - conn.set_volume(volume).await.map_err(|e| e.to_string()) -} - -async fn download_audio(url: &str) -> Result, String> { - use std::time::Duration; - - let client = reqwest::Client::builder() - .timeout(Duration::from_secs(120)) - .connect_timeout(Duration::from_secs(10)) - .build() - .map_err(|e| format!("Failed to create HTTP client: {}", e))?; - - let response = client - .get(url) - .header("User-Agent", "Mozilla/5.0") - .send() - .await - .map_err(|e| format!("Failed to fetch audio: {}", e))?; - - if !response.status().is_success() { - return Err(format!("HTTP error: {}", response.status())); - } - - let bytes = response - .bytes() - .await - .map_err(|e| format!("Failed to read audio bytes: {}", e))?; - - Ok(bytes.to_vec()) -} diff --git a/src-tauri/src/cast/dlna/mod.rs b/src-tauri/src/cast/dlna/mod.rs index ad6cafc58..101436991 100644 --- a/src-tauri/src/cast/dlna/mod.rs +++ b/src-tauri/src/cast/dlna/mod.rs @@ -2,7 +2,6 @@ //! //! Core functionality is in qbz-cast crate, this module provides Tauri commands. -pub mod commands; pub mod state; // Re-export from qbz-cast diff --git a/src-tauri/src/cast/mod.rs b/src-tauri/src/cast/mod.rs index 86812b641..31ac70e8b 100644 --- a/src-tauri/src/cast/mod.rs +++ b/src-tauri/src/cast/mod.rs @@ -4,7 +4,6 @@ //! The core casting logic is in the qbz-cast crate. pub mod chromecast_thread; -pub mod commands; pub mod device; pub mod discovery; pub mod dlna;