584 lines
18 KiB
Rust
584 lines
18 KiB
Rust
// Tauri commands for repository access
|
|
// Uses handle-based system: UUID -> Arc<HybridRepository>
|
|
|
|
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<Mutex<HashMap<String, Arc<HybridRepository>>>>,
|
|
}
|
|
|
|
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<Arc<HybridRepository>> {
|
|
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<String, String> {
|
|
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<Vec<Library>, 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<GetItemsOptions>,
|
|
) -> Result<SearchResult, String> {
|
|
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<MediaItem, String> {
|
|
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<usize>,
|
|
) -> Result<Vec<MediaItem>, 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<String>,
|
|
limit: Option<usize>,
|
|
) -> Result<Vec<MediaItem>, 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<String>,
|
|
limit: Option<usize>,
|
|
) -> Result<Vec<MediaItem>, 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<usize>,
|
|
) -> Result<Vec<MediaItem>, 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<usize>,
|
|
) -> Result<Vec<MediaItem>, 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<String>,
|
|
) -> Result<Vec<Genre>, 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<SearchOptions>,
|
|
) -> Result<SearchResult, String> {
|
|
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<PlaybackInfo, String> {
|
|
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<String>,
|
|
start_time_seconds: Option<f64>,
|
|
audio_stream_index: Option<i32>,
|
|
) -> Result<String, String> {
|
|
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<String, String> {
|
|
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<ImageOptions>,
|
|
) -> Result<String, String> {
|
|
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<String, String> {
|
|
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<String>,
|
|
) -> Result<String, String> {
|
|
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<MediaItem, String> {
|
|
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<GetItemsOptions>,
|
|
) -> Result<SearchResult, String> {
|
|
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<usize>,
|
|
) -> Result<SearchResult, String> {
|
|
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::<RepositoryManager>() > 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::<RepositoryManagerWrapper>() > 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<T: Send + Sync>() {}
|
|
is_send_sync::<RepositoryManager>();
|
|
}
|
|
|
|
#[test]
|
|
fn test_repository_manager_wrapper_is_send_sync() {
|
|
// Verify RepositoryManagerWrapper is Send + Sync
|
|
fn is_send_sync<T: Send + Sync>() {}
|
|
is_send_sync::<RepositoryManagerWrapper>();
|
|
}
|
|
|
|
#[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();
|
|
}
|
|
}
|
|
}
|