jellytau/src-tauri/src/commands/repository.rs
Duncan Tourolle 59270e8a4f
Some checks failed
Traceability Validation / Check Requirement Traces (push) Has been cancelled
🏗️ Build and Test JellyTau / Build APK and Run Tests (push) Has been cancelled
fix tests
2026-02-14 16:39:46 +01:00

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();
}
}
}