//! Tauri commands for playback reporting operations //! //! TRACES: UR-025, UR-019 | IR-015, JA-010, JA-011, JA-012 | DR-028 //! //! These commands provide frontend access to the Rust playback reporting system, //! replacing the TypeScript implementation with native Rust reporting. //! //! Commands are registered but not yet called from the frontend. //! Dead code warnings are suppressed until frontend migration is complete. #![allow(dead_code)] use std::sync::Arc; use tauri::State; use tokio::sync::Mutex as TokioMutex; use crate::commands::connectivity::ConnectivityMonitorWrapper; use crate::commands::storage::DatabaseWrapper; use crate::jellyfin::client::JellyfinClient; use crate::jellyfin::JellyfinConfig; use crate::playback_reporting::{PlaybackContext, PlaybackOperation, PlaybackReporter}; use crate::utils::conversions::seconds_to_ticks; /// Tauri state wrapper for PlaybackReporter pub struct PlaybackReporterWrapper(pub Arc>>); /// Initialize playback reporter (called after login) #[tauri::command] #[specta::specta] pub async fn playback_reporter_init( reporter_wrapper: State<'_, PlaybackReporterWrapper>, db: State<'_, DatabaseWrapper>, server_url: String, user_id: String, access_token: String, device_id: String, ) -> Result<(), String> { log::info!("[PlaybackReporter] Initializing for user: {}", user_id); // Get database service let db_service = { let database = db.0.lock().map_err(|e| e.to_string())?; Arc::new(database.service()) }; // Create JellyfinClient let jellyfin_config = JellyfinConfig { server_url, access_token, device_id, }; let jellyfin_client = JellyfinClient::new(jellyfin_config) .map_err(|e| format!("Failed to create JellyfinClient: {}", e))?; // Create PlaybackReporter let reporter = PlaybackReporter::new( db_service, Arc::new(TokioMutex::new(Some(jellyfin_client))), user_id.clone(), ); // Store in wrapper *reporter_wrapper.0.lock().await = Some(reporter); log::info!( "[PlaybackReporter] Initialized successfully for user: {}", user_id ); Ok(()) } /// Destroy playback reporter (called on logout) #[tauri::command] #[specta::specta] pub async fn playback_reporter_destroy( reporter_wrapper: State<'_, PlaybackReporterWrapper>, ) -> Result<(), String> { log::info!("[PlaybackReporter] Destroying reporter"); *reporter_wrapper.0.lock().await = None; Ok(()) } /// Report playback start #[tauri::command] #[specta::specta] pub async fn playback_report_start( reporter: State<'_, PlaybackReporterWrapper>, connectivity: State<'_, ConnectivityMonitorWrapper>, item_id: String, position_seconds: f64, context_type: Option, context_id: Option, ) -> Result<(), String> { let reporter_guard = reporter.0.lock().await; let reporter_instance = reporter_guard .as_ref() .ok_or("PlaybackReporter not initialized")?; let position_ticks = seconds_to_ticks(position_seconds); let context = context_type.map(|ct| PlaybackContext { context_type: ct, context_id, }); let operation = PlaybackOperation::Start { item_id, position_ticks, context, }; let monitor = connectivity.0.lock().await; let is_online = monitor.get_status().await.is_server_reachable; drop(monitor); reporter_instance.report(operation, is_online).await } /// Report playback progress #[tauri::command] #[specta::specta] pub async fn playback_report_progress( reporter: State<'_, PlaybackReporterWrapper>, connectivity: State<'_, ConnectivityMonitorWrapper>, item_id: String, position_seconds: f64, is_paused: bool, ) -> Result<(), String> { let reporter_guard = reporter.0.lock().await; let reporter_instance = reporter_guard .as_ref() .ok_or("PlaybackReporter not initialized")?; let position_ticks = seconds_to_ticks(position_seconds); let operation = PlaybackOperation::Progress { item_id, position_ticks, is_paused, }; let monitor = connectivity.0.lock().await; let is_online = monitor.get_status().await.is_server_reachable; drop(monitor); reporter_instance.report(operation, is_online).await } /// Report playback stopped #[tauri::command] #[specta::specta] pub async fn playback_report_stopped( reporter: State<'_, PlaybackReporterWrapper>, connectivity: State<'_, ConnectivityMonitorWrapper>, item_id: String, position_seconds: f64, ) -> Result<(), String> { let reporter_guard = reporter.0.lock().await; let reporter_instance = reporter_guard .as_ref() .ok_or("PlaybackReporter not initialized")?; let position_ticks = seconds_to_ticks(position_seconds); let operation = PlaybackOperation::Stopped { item_id, position_ticks, }; let monitor = connectivity.0.lock().await; let is_online = monitor.get_status().await.is_server_reachable; drop(monitor); reporter_instance.report(operation, is_online).await } /// Mark item as played #[tauri::command] #[specta::specta] pub async fn playback_mark_played( reporter: State<'_, PlaybackReporterWrapper>, connectivity: State<'_, ConnectivityMonitorWrapper>, item_id: String, ) -> Result<(), String> { let reporter_guard = reporter.0.lock().await; let reporter_instance = reporter_guard .as_ref() .ok_or("PlaybackReporter not initialized")?; let operation = PlaybackOperation::MarkPlayed { item_id }; let monitor = connectivity.0.lock().await; let is_online = monitor.get_status().await.is_server_reachable; drop(monitor); reporter_instance.report(operation, is_online).await } #[cfg(test)] mod tests { use super::*; #[test] fn test_playback_operation_start_creation() { let operation = PlaybackOperation::Start { item_id: "item-123".to_string(), position_ticks: 15_000_000, context: Some(PlaybackContext { context_type: "series".to_string(), context_id: Some("series-456".to_string()), }), }; // Verify enum variant can be created and pattern matched if let PlaybackOperation::Start { item_id, position_ticks, context, } = operation { assert_eq!(item_id, "item-123"); assert_eq!(position_ticks, 15_000_000); assert!(context.is_some()); let ctx = context.unwrap(); assert_eq!(ctx.context_type, "series"); assert_eq!(ctx.context_id, Some("series-456".to_string())); } else { panic!("Expected Start variant"); } } #[test] fn test_playback_operation_start_without_context() { let operation = PlaybackOperation::Start { item_id: "item-789".to_string(), position_ticks: 5_000_000, context: None, }; if let PlaybackOperation::Start { item_id, context, .. } = operation { assert_eq!(item_id, "item-789"); assert!(context.is_none()); } else { panic!("Expected Start variant"); } } #[test] fn test_playback_operation_progress_creation() { let operation = PlaybackOperation::Progress { item_id: "item-999".to_string(), position_ticks: 30_000_000, is_paused: true, }; if let PlaybackOperation::Progress { item_id, position_ticks, is_paused, } = operation { assert_eq!(item_id, "item-999"); assert_eq!(position_ticks, 30_000_000); assert!(is_paused); } else { panic!("Expected Progress variant"); } } #[test] fn test_playback_operation_progress_playing() { let operation = PlaybackOperation::Progress { item_id: "item-555".to_string(), position_ticks: 45_000_000, is_paused: false, }; if let PlaybackOperation::Progress { is_paused, .. } = operation { assert!(!is_paused); } else { panic!("Expected Progress variant"); } } #[test] fn test_playback_operation_stopped_creation() { let operation = PlaybackOperation::Stopped { item_id: "item-111".to_string(), position_ticks: 120_000_000, }; if let PlaybackOperation::Stopped { item_id, position_ticks, } = operation { assert_eq!(item_id, "item-111"); assert_eq!(position_ticks, 120_000_000); } else { panic!("Expected Stopped variant"); } } #[test] fn test_playback_operation_mark_played_creation() { let operation = PlaybackOperation::MarkPlayed { item_id: "item-222".to_string(), }; if let PlaybackOperation::MarkPlayed { item_id } = operation { assert_eq!(item_id, "item-222"); } else { panic!("Expected MarkPlayed variant"); } } #[test] fn test_playback_context_with_series() { let context = PlaybackContext { context_type: "series".to_string(), context_id: Some("series-789".to_string()), }; assert_eq!(context.context_type, "series"); assert_eq!(context.context_id, Some("series-789".to_string())); } #[test] fn test_playback_context_without_id() { let context = PlaybackContext { context_type: "folder".to_string(), context_id: None, }; assert_eq!(context.context_type, "folder"); assert!(context.context_id.is_none()); } #[test] fn test_playback_context_clone() { let context = PlaybackContext { context_type: "container".to_string(), context_id: Some("container-123".to_string()), }; let cloned = context.clone(); assert_eq!(cloned.context_type, "container"); assert_eq!(cloned.context_id, Some("container-123".to_string())); } #[test] fn test_seconds_to_ticks_conversion() { assert_eq!(seconds_to_ticks(0.0), 0); assert_eq!(seconds_to_ticks(1.0), 10_000_000); assert_eq!(seconds_to_ticks(1.5), 15_000_000); assert_eq!(seconds_to_ticks(120.0), 1_200_000_000); } #[test] fn test_playback_reporter_wrapper_structure() { // Verify wrapper type can hold Arc>> assert!(std::mem::size_of::() > 0); } #[test] fn test_playback_operation_debug_trait() { // Verify Debug trait is implemented for operations let operation = PlaybackOperation::Start { item_id: "item-1".to_string(), position_ticks: 0, context: None, }; let debug_str = format!("{:?}", operation); assert!(debug_str.contains("Start")); assert!(debug_str.contains("item-1")); } #[test] fn test_playback_operation_clone() { let operation = PlaybackOperation::Progress { item_id: "item-clone".to_string(), position_ticks: 50_000_000, is_paused: true, }; let cloned = operation.clone(); if let PlaybackOperation::Progress { item_id, is_paused, .. } = cloned { assert_eq!(item_id, "item-clone"); assert!(is_paused); } else { panic!("Clone failed to preserve variant"); } } #[test] fn test_playback_operation_all_variants() { // Test that all operation variants can be created and matched let start_op = PlaybackOperation::Start { item_id: "i1".to_string(), position_ticks: 0, context: None, }; assert!(matches!(start_op, PlaybackOperation::Start { .. })); let progress_op = PlaybackOperation::Progress { item_id: "i2".to_string(), position_ticks: 100, is_paused: false, }; assert!(matches!(progress_op, PlaybackOperation::Progress { .. })); let stopped_op = PlaybackOperation::Stopped { item_id: "i3".to_string(), position_ticks: 200, }; assert!(matches!(stopped_op, PlaybackOperation::Stopped { .. })); let played_op = PlaybackOperation::MarkPlayed { item_id: "i4".to_string(), }; assert!(matches!(played_op, PlaybackOperation::MarkPlayed { .. })); } }