use std::sync::Arc; use tauri::State; use crate::playback_mode::{PlaybackMode, PlaybackModeManager}; /// Wrapper for PlaybackModeManager to manage in Tauri state pub struct PlaybackModeManagerWrapper(pub Arc); /// Get the current playback mode #[tauri::command] #[specta::specta] pub fn playback_mode_get_current( manager: State<'_, PlaybackModeManagerWrapper>, ) -> Result { Ok(manager.0.get_mode()) } /// Set the playback mode (internal/testing use) #[tauri::command] #[specta::specta] pub fn playback_mode_set( manager: State<'_, PlaybackModeManagerWrapper>, mode: PlaybackMode, ) -> Result<(), String> { manager.0.set_mode(mode); Ok(()) } /// Check if currently transferring between playback modes #[tauri::command] #[specta::specta] pub fn playback_mode_is_transferring( manager: State<'_, PlaybackModeManagerWrapper>, ) -> Result { Ok(manager.0.is_transferring()) } /// Transfer playback from local device to a remote Jellyfin session #[tauri::command] #[specta::specta] pub async fn playback_mode_transfer_to_remote( manager: State<'_, PlaybackModeManagerWrapper>, session_id: String, position: Option, ) -> Result<(), String> { log::info!( "[PlaybackModeCommands] Transferring to remote session: {} (position override: {:?})", session_id, position ); manager.0.transfer_to_remote(session_id, position).await } /// Transfer playback from remote session back to local device /// /// Parameters: /// - current_item_id: The Jellyfin item ID currently playing on remote /// - position_ticks: Current playback position in ticks (10,000 ticks = 1ms) #[tauri::command] #[specta::specta] pub async fn playback_mode_transfer_to_local( manager: State<'_, PlaybackModeManagerWrapper>, current_item_id: String, position_ticks: i64, ) -> Result<(), String> { log::info!( "[PlaybackModeCommands] Transferring to local: item_id={}, position={}", current_item_id, position_ticks ); manager .0 .transfer_to_local(current_item_id, position_ticks) .await } /// Get remote session status (for polling position/duration) #[tauri::command] #[specta::specta] pub async fn playback_mode_get_remote_status( manager: State<'_, PlaybackModeManagerWrapper>, player: State<'_, crate::commands::PlayerStateWrapper>, ) -> Result { let mode = manager.0.get_mode(); if let crate::playback_mode::PlaybackMode::Remote { session_id } = mode { // Get Jellyfin client from player controller - clone before await let client = { let controller = player.0.lock().await; let client_arc = controller.jellyfin_client(); let client_opt = client_arc.lock().map_err(|e| e.to_string())?; client_opt.as_ref().ok_or("Jellyfin client not configured")?.clone() }; // Get session info match client.get_session(&session_id).await { Ok(Some(session)) => { let position_ticks = session.play_state.as_ref() .and_then(|ps| ps.position_ticks) .unwrap_or(0); let duration_ticks = session.now_playing_item.as_ref() .and_then(|item| item.run_time_ticks) .unwrap_or(0); let is_paused = session.play_state.as_ref() .and_then(|ps| ps.is_paused) .unwrap_or(true); Ok(RemoteSessionStatus { position: position_ticks as f64 / 10_000_000.0, duration: if duration_ticks > 0 { Some(duration_ticks as f64 / 10_000_000.0) } else { None }, is_playing: !is_paused, now_playing_item: session.now_playing_item.clone(), }) } Ok(None) => Err("Remote session not found".to_string()), Err(e) => Err(format!("Failed to get session status: {}", e)), } } else { Err("Not in remote playback mode".to_string()) } } /// Remote session status for UI updates #[derive(specta::Type, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct RemoteSessionStatus { pub position: f64, pub duration: Option, pub is_playing: bool, pub now_playing_item: Option, } #[cfg(test)] mod tests { use super::*; #[test] fn test_playback_mode_serialization() { // Test Local playback mode let local_mode = PlaybackMode::Local; let json = serde_json::to_string(&local_mode); assert!(json.is_ok()); // Test Remote playback mode let remote_mode = PlaybackMode::Remote { session_id: "session-123".to_string(), }; let json = serde_json::to_string(&remote_mode); assert!(json.is_ok()); } #[test] fn test_remote_session_status_serialization() { let status = RemoteSessionStatus { position: 123.45, duration: Some(600.0), is_playing: true, now_playing_item: None, }; // Should serialize successfully let json = serde_json::to_string(&status); assert!(json.is_ok()); let serialized = json.unwrap(); assert!(serialized.contains("123.45")); assert!(serialized.contains("600")); assert!(serialized.contains("true")); } #[test] fn test_remote_session_status_with_no_duration() { let status = RemoteSessionStatus { position: 0.0, duration: None, is_playing: false, now_playing_item: None, }; let json = serde_json::to_string(&status).unwrap(); assert!(json.contains("null") || json.contains("\"duration\":null")); } #[test] fn test_remote_session_status_various_positions() { let positions = vec![0.0, 30.5, 100.0, 3600.0]; for pos in positions { let status = RemoteSessionStatus { position: pos, duration: Some(7200.0), is_playing: true, now_playing_item: None, }; let json = serde_json::to_string(&status).unwrap(); assert!(json.contains(&pos.to_string())); } } #[test] fn test_playback_mode_deserialization_from_frontend() { // Test what frontend sends for Idle mode let idle_json = r#"{"type":"idle"}"#; let mode: PlaybackMode = serde_json::from_str(idle_json).expect("Failed to deserialize idle"); assert_eq!(mode, PlaybackMode::Idle); // Test what frontend sends for Local mode let local_json = r#"{"type":"local"}"#; let mode: PlaybackMode = serde_json::from_str(local_json).expect("Failed to deserialize local"); assert_eq!(mode, PlaybackMode::Local); // Test what frontend sends for Remote mode let remote_json = r#"{"type":"remote","session_id":"session-123"}"#; let mode: PlaybackMode = serde_json::from_str(remote_json).expect("Failed to deserialize remote"); match mode { PlaybackMode::Remote { session_id } => assert_eq!(session_id, "session-123"), _ => panic!("Expected Remote mode"), } } #[test] fn test_play_tracks_context_deserialization() { use crate::commands::PlayTracksContext; // Test Search context (the recently fixed issue) let search_json = r#"{"type":"search","searchQuery":"test query"}"#; let context: PlayTracksContext = serde_json::from_str(search_json) .expect("Failed to deserialize search context"); match context { PlayTracksContext::Search { search_query } => { assert_eq!(search_query, "test query"); } _ => panic!("Expected Search context"), } // Test Playlist context let playlist_json = r#"{"type":"playlist","playlistId":"pl-123","playlistName":"My Playlist"}"#; let context: PlayTracksContext = serde_json::from_str(playlist_json) .expect("Failed to deserialize playlist context"); match context { PlayTracksContext::Playlist { playlist_id, playlist_name } => { assert_eq!(playlist_id, "pl-123"); assert_eq!(playlist_name, "My Playlist"); } _ => panic!("Expected Playlist context"), } // Test Custom context let custom_json = r#"{"type":"custom","label":"Custom Queue"}"#; let context: PlayTracksContext = serde_json::from_str(custom_json) .expect("Failed to deserialize custom context"); match context { PlayTracksContext::Custom { label } => { assert_eq!(label, Some("Custom Queue".to_string())); } _ => panic!("Expected Custom context"), } } }