// Tauri commands for repository access // Uses handle-based system: UUID -> Arc use std::collections::HashMap; use std::sync::{Arc, Mutex}; use log::{debug, error, info}; use tauri::State; use uuid::Uuid; use crate::jellyfin::HttpClient; use crate::repository::{HybridRepository, MediaRepository, OnlineRepository, OfflineRepository, types::*}; /// Repository handle manager pub struct RepositoryManager { repositories: Arc>>>, } impl RepositoryManager { pub fn new() -> Self { Self { repositories: Arc::new(Mutex::new(HashMap::new())), } } pub fn create(&self, handle: String, repository: HybridRepository) { let mut repos = self.repositories.lock().unwrap(); repos.insert(handle, Arc::new(repository)); } pub fn get(&self, handle: &str) -> Option> { let repos = self.repositories.lock().unwrap(); repos.get(handle).cloned() } pub fn destroy(&self, handle: &str) { let mut repos = self.repositories.lock().unwrap(); repos.remove(handle); } } /// Wrapper for Tauri state pub struct RepositoryManagerWrapper(pub RepositoryManager); /// Create a new repository instance /// Returns a handle (UUID) for accessing the repository #[tauri::command] pub async fn repository_create( manager: State<'_, RepositoryManagerWrapper>, db: State<'_, crate::commands::storage::DatabaseWrapper>, server_url: String, user_id: String, access_token: String, server_id: String, ) -> Result { info!("[REPO] repository_create called for user: {}", user_id); // Create HTTP client for online repository debug!("[REPO] Creating HTTP client..."); let http_config = crate::jellyfin::HttpConfig::default(); let http_client = HttpClient::new(http_config).map_err(|e| { error!("[REPO] HTTP client creation failed: {}", e); e.to_string() })?; debug!("[REPO] HTTP client created successfully"); // Create online repository debug!("[REPO] Creating online repository..."); let online = OnlineRepository::new(Arc::new(http_client), server_url, user_id.clone(), access_token); debug!("[REPO] Online repository created"); // Create offline repository with async-safe database service debug!("[REPO] Creating database service..."); let db_service = { let database = db.0.lock().map_err(|e| { error!("[REPO] Database lock failed: {}", e); e.to_string() })?; debug!("[REPO] Database lock acquired, getting service..."); Arc::new(database.service()) }; // Lock is released here debug!("[REPO] Database service created"); debug!("[REPO] Creating offline repository..."); let offline = OfflineRepository::new(db_service, server_id, user_id); debug!("[REPO] Offline repository created"); // Create hybrid repository debug!("[REPO] Creating hybrid repository..."); let hybrid = HybridRepository::new(online, offline); debug!("[REPO] Hybrid repository created"); // Generate handle and store repository let uuid = Uuid::new_v4(); let handle = format!("{}", uuid); info!("[REPO] Generated handle: {}", handle); // Store repository synchronously debug!("[REPO] Storing repository..."); manager.0.create(handle.clone(), hybrid); info!("[REPO] Repository stored successfully"); Ok(handle) } /// Destroy a repository instance #[tauri::command] pub async fn repository_destroy( manager: State<'_, RepositoryManagerWrapper>, handle: String, ) -> Result<(), String> { manager.0.destroy(&handle); Ok(()) } /// Get libraries #[tauri::command] pub async fn repository_get_libraries( manager: State<'_, RepositoryManagerWrapper>, handle: String, ) -> Result, String> { debug!("[REPO] get_libraries called with handle: {}", handle); let repo = manager.0.get(&handle).ok_or_else(|| { error!("[REPO] Repository not found for handle: {}", handle); "Repository not found".to_string() })?; debug!("[REPO] Repository found, fetching libraries..."); repo.as_ref().get_libraries() .await .map_err(|e| { error!("[REPO] Error fetching libraries: {:?}", e); format!("{:?}", e) }) } /// Get items in a container (library, folder, album, etc.) #[tauri::command] pub async fn repository_get_items( manager: State<'_, RepositoryManagerWrapper>, handle: String, parent_id: String, options: Option, ) -> Result { let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref().get_items(&parent_id, options) .await .map_err(|e| format!("{:?}", e)) } /// Get a single item by ID #[tauri::command] pub async fn repository_get_item( manager: State<'_, RepositoryManagerWrapper>, handle: String, item_id: String, ) -> Result { let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref().get_item(&item_id) .await .map_err(|e| format!("{:?}", e)) } /// Get latest items in a library #[tauri::command] pub async fn repository_get_latest_items( manager: State<'_, RepositoryManagerWrapper>, handle: String, parent_id: String, limit: Option, ) -> Result, String> { let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref().get_latest_items(&parent_id, limit) .await .map_err(|e| format!("{:?}", e)) } /// Get resume items (continue watching/listening) #[tauri::command] pub async fn repository_get_resume_items( manager: State<'_, RepositoryManagerWrapper>, handle: String, parent_id: Option, limit: Option, ) -> Result, String> { debug!("[REPO] get_resume_items called with handle: {}", handle); let repo = manager.0.get(&handle).ok_or_else(|| { error!("[REPO] Repository not found for handle: {}", handle); "Repository not found".to_string() })?; debug!("[REPO] Repository found, fetching resume items..."); repo.as_ref().get_resume_items(parent_id.as_deref(), limit) .await .map_err(|e| { error!("[REPO] Error fetching resume items: {:?}", e); format!("{:?}", e) }) } /// Get next up episodes #[tauri::command] pub async fn repository_get_next_up_episodes( manager: State<'_, RepositoryManagerWrapper>, handle: String, series_id: Option, limit: Option, ) -> Result, String> { let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref().get_next_up_episodes(series_id.as_deref(), limit) .await .map_err(|e| format!("{:?}", e)) } /// Get recently played audio #[tauri::command] pub async fn repository_get_recently_played_audio( manager: State<'_, RepositoryManagerWrapper>, handle: String, limit: Option, ) -> Result, String> { let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref().get_recently_played_audio(limit) .await .map_err(|e| format!("{:?}", e)) } /// Get resume movies #[tauri::command] pub async fn repository_get_resume_movies( manager: State<'_, RepositoryManagerWrapper>, handle: String, limit: Option, ) -> Result, String> { let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref().get_resume_movies(limit) .await .map_err(|e| format!("{:?}", e)) } /// Get genres for a library #[tauri::command] pub async fn repository_get_genres( manager: State<'_, RepositoryManagerWrapper>, handle: String, parent_id: Option, ) -> Result, String> { let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref().get_genres(parent_id.as_deref()) .await .map_err(|e| format!("{:?}", e)) } /// Search for items #[tauri::command] pub async fn repository_search( manager: State<'_, RepositoryManagerWrapper>, handle: String, query: String, options: Option, ) -> Result { let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref().search(&query, options) .await .map_err(|e| format!("{:?}", e)) } /// Get playback info for an item #[tauri::command] pub async fn repository_get_playback_info( manager: State<'_, RepositoryManagerWrapper>, handle: String, item_id: String, ) -> Result { let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref().get_playback_info(&item_id) .await .map_err(|e| format!("{:?}", e)) } /// Get video stream URL with optional seeking support #[tauri::command] pub async fn repository_get_video_stream_url( manager: State<'_, RepositoryManagerWrapper>, handle: String, item_id: String, media_source_id: Option, start_time_seconds: Option, audio_stream_index: Option, ) -> Result { let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref() .get_video_stream_url( &item_id, media_source_id.as_deref(), start_time_seconds, audio_stream_index, ) .await .map_err(|e| format!("{:?}", e)) } /// Get audio stream URL for a track #[tauri::command] pub async fn repository_get_audio_stream_url( manager: State<'_, RepositoryManagerWrapper>, handle: String, item_id: String, ) -> Result { let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref() .get_audio_stream_url(&item_id) .await .map_err(|e| format!("{:?}", e)) } /// Report playback start #[tauri::command] pub async fn repository_report_playback_start( manager: State<'_, RepositoryManagerWrapper>, handle: String, item_id: String, position_ticks: i64, ) -> Result<(), String> { let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref().report_playback_start(&item_id, position_ticks) .await .map_err(|e| format!("{:?}", e)) } /// Report playback progress #[tauri::command] pub async fn repository_report_playback_progress( manager: State<'_, RepositoryManagerWrapper>, handle: String, item_id: String, position_ticks: i64, ) -> Result<(), String> { let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref().report_playback_progress(&item_id, position_ticks) .await .map_err(|e| format!("{:?}", e)) } /// Report playback stopped #[tauri::command] pub async fn repository_report_playback_stopped( manager: State<'_, RepositoryManagerWrapper>, handle: String, item_id: String, position_ticks: i64, ) -> Result<(), String> { let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref().report_playback_stopped(&item_id, position_ticks) .await .map_err(|e| format!("{:?}", e)) } /// Get image URL for an item #[tauri::command] pub fn repository_get_image_url( manager: State<'_, RepositoryManagerWrapper>, handle: String, item_id: String, image_type: ImageType, options: Option, ) -> Result { let repo = manager.0.get(&handle).ok_or("Repository not found")?; Ok(repo.as_ref().get_image_url(&item_id, image_type, options)) } /// Get subtitle URL for a media item #[tauri::command] #[allow(dead_code)] pub fn repository_get_subtitle_url( manager: State<'_, RepositoryManagerWrapper>, handle: String, item_id: String, media_source_id: String, stream_index: i32, format: String, ) -> Result { let repo = manager.0.get(&handle).ok_or("Repository not found")?; Ok(repo.as_ref().get_subtitle_url(&item_id, &media_source_id, stream_index, &format)) } /// Get video download URL with quality preset #[tauri::command] #[allow(dead_code)] pub fn repository_get_video_download_url( manager: State<'_, RepositoryManagerWrapper>, handle: String, item_id: String, quality: String, media_source_id: Option, ) -> Result { let repo = manager.0.get(&handle).ok_or("Repository not found")?; Ok(repo.as_ref().get_video_download_url(&item_id, &quality, media_source_id.as_deref())) } /// Mark an item as favorite #[tauri::command] pub async fn repository_mark_favorite( manager: State<'_, RepositoryManagerWrapper>, handle: String, item_id: String, ) -> Result<(), String> { let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref().mark_favorite(&item_id) .await .map_err(|e| format!("{:?}", e)) } /// Unmark an item as favorite #[tauri::command] pub async fn repository_unmark_favorite( manager: State<'_, RepositoryManagerWrapper>, handle: String, item_id: String, ) -> Result<(), String> { let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref().unmark_favorite(&item_id) .await .map_err(|e| format!("{:?}", e)) } /// Get person details #[tauri::command] pub async fn repository_get_person( manager: State<'_, RepositoryManagerWrapper>, handle: String, person_id: String, ) -> Result { let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref().get_person(&person_id) .await .map_err(|e| format!("{:?}", e)) } /// Get items by person (actor, director, etc.) #[tauri::command] pub async fn repository_get_items_by_person( manager: State<'_, RepositoryManagerWrapper>, handle: String, person_id: String, options: Option, ) -> Result { let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref().get_items_by_person(&person_id, options) .await .map_err(|e| format!("{:?}", e)) } /// Get similar/related items for a media item #[tauri::command] pub async fn repository_get_similar_items( manager: State<'_, RepositoryManagerWrapper>, handle: String, item_id: String, limit: Option, ) -> Result { let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref().get_similar_items(&item_id, limit) .await .map_err(|e| format!("{:?}", e)) } #[cfg(test)] mod tests { use super::*; #[test] fn test_repository_manager_creation() { let manager = RepositoryManager::new(); // Should be able to create RepositoryManager without panicking assert_eq!(std::mem::size_of::() > 0, true); } #[test] fn test_repository_manager_wrapper_structure() { let manager = RepositoryManager::new(); let wrapper = RepositoryManagerWrapper(manager); // Verify wrapper holds the manager assert_eq!(std::mem::size_of::() > 0, true); } #[test] fn test_repository_manager_get_nonexistent() { let manager = RepositoryManager::new(); // Getting a non-existent repository should return None let result = manager.get("nonexistent-handle"); assert!(result.is_none()); } #[test] fn test_uuid_handle_generation() { let uuid = Uuid::new_v4(); let handle = format!("{}", uuid); // UUID should convert to a non-empty string assert!(!handle.is_empty()); assert!(handle.len() > 0); } #[test] fn test_uuid_handles_are_unique() { let handle1 = format!("{}", Uuid::new_v4()); let handle2 = format!("{}", Uuid::new_v4()); // Two generated UUIDs should be different assert_ne!(handle1, handle2); } #[test] fn test_uuid_handle_format() { let uuid = Uuid::new_v4(); let handle = format!("{}", uuid); // UUID should have standard format with hyphens let parts: Vec<&str> = handle.split('-').collect(); assert_eq!(parts.len(), 5); } #[test] fn test_repository_manager_destroy_nonexistent() { let manager = RepositoryManager::new(); // Destroying a non-existent repository should not panic manager.destroy("nonexistent-handle"); } #[test] fn test_repository_manager_is_send_sync() { // Verify RepositoryManager can be used in async contexts fn is_send_sync() {} is_send_sync::(); } #[test] fn test_repository_manager_wrapper_is_send_sync() { // Verify RepositoryManagerWrapper is Send + Sync fn is_send_sync() {} is_send_sync::(); } #[test] fn test_multiple_manager_instances() { let manager1 = RepositoryManager::new(); let manager2 = RepositoryManager::new(); // Multiple manager instances should be independent let handle1_nonexistent = manager1.get("test"); let handle2_nonexistent = manager2.get("test"); assert!(handle1_nonexistent.is_none()); assert!(handle2_nonexistent.is_none()); } #[test] fn test_handle_string_properties() { let uuid = Uuid::new_v4(); let handle = format!("{}", uuid); // Handle should be alphanumeric with hyphens for c in handle.chars() { assert!(c.is_alphanumeric() || c == '-'); } } #[test] fn test_repository_manager_concurrent_access() { let manager = Arc::new(RepositoryManager::new()); let mut handles = vec![]; // Verify manager can be wrapped in Arc for concurrent access for _ in 0..3 { let mgr = Arc::clone(&manager); let handle = std::thread::spawn(move || { let result = mgr.get("test"); assert!(result.is_none()); }); handles.push(handle); } for h in handles { h.join().unwrap(); } } }