use std::path::{Path, PathBuf}; use movie_mapper::model::file::MediaFile; use movie_mapper::service::file_scanner::FileScanner; use movie_mapper::service::file_mapper::FileMapper; use movie_mapper::service::audit_logger::{AuditLogger, AuditAction}; use movie_mapper::service::tvdb_api::TVDBClient; use crate::BackendError; /// Backend state management #[derive(Debug, Clone, Default)] pub struct BackendState { /// Currently scanned files pub files: Vec, /// Currently selected directory pub current_directory: Option, /// Navigation stack for back button pub navigation_stack: Vec, /// Tagged files pub tagged_files: Vec, /// TVDB API client (if configured) pub tvdb_client: Option, } /// TVDB client state #[derive(Debug, Clone)] pub struct TVDBClientState { /// API key pub api_key: String, /// Auth token (if authenticated) pub auth_token: Option, /// Is authenticated pub authenticated: bool, } impl Default for TVDBClientState { fn default() -> Self { Self { api_key: String::new(), auth_token: None, authenticated: false, } } } /// Backend integration functions impl BackendState { /// Create a new backend state pub fn new() -> Self { Self::default() } /// Scan a directory for media files /// /// # Arguments /// * `path` - Directory path to scan /// * `progress_callback` - Optional callback for progress updates /// /// # Returns /// * `Ok(Vec)` - List of scanned files /// * `Err(BackendError)` - Error if scanning failed pub async fn scan_directory( &mut self, path: &Path, progress_callback: Option<&mut dyn FnMut(usize, usize, &str)>, ) -> Result, BackendError> { let scanner = FileScanner::new(); let files = scanner.scan_directory(path, progress_callback).await .map_err(|e| BackendError::General(e.to_string()))?; // Update state self.current_directory = Some(path.to_path_buf()); self.files = files.clone(); Ok(files) } /// Rename a file /// /// # Arguments /// * `old_path` - Current file path /// * `new_path` - New file path /// /// # Returns /// * `Ok(())` - Success /// * `Err(BackendError)` - Error if rename failed pub async fn rename_file(&self, old_path: &Path, new_path: &Path) -> Result<(), BackendError> { // Clone paths for spawn_blocking let old_path = old_path.to_path_buf(); let new_path = new_path.to_path_buf(); // Use tokio::task::spawn_blocking for blocking I/O operations tokio::task::spawn_blocking(move || { std::fs::rename(&old_path, &new_path) .map_err(|e| BackendError::from(e.to_string())) }) .await .map_err(|e| BackendError::from(format!("Task error: {}", e)))? } /// Move a file to a folder (Jellyfin compatible) /// /// # Arguments /// * `source_path` - Source file path /// * `folder_name` - Target folder name (e.g., "extras", "commentary") /// /// # Returns /// * `Ok(PathBuf)` - New file path /// * `Err(BackendError)` - Error if move failed pub async fn move_to_folder( &self, source_path: &Path, folder_name: &str, ) -> Result { let folder_path = source_path .parent() .ok_or_else(|| BackendError::General("No parent directory".to_string()))?; let target_dir_original = folder_path.join(folder_name); let target_dir = target_dir_original.clone(); // Clone paths for spawn_blocking let source_path = source_path.to_path_buf(); // Create folder if it doesn't exist tokio::task::spawn_blocking(move || { std::fs::create_dir_all(&target_dir) .map_err(|e| BackendError::from(format!("Failed to create directory: {}", e))) }) .await .map_err(|e| BackendError::from(format!("Task error: {}", e)))?; let file_name = source_path .file_name() .ok_or_else(|| BackendError::General("No file name".to_string()))?; let file_name_clone = file_name.to_os_string(); let target_path = target_dir_original.join(&file_name_clone); let target_path_clone = target_path.clone(); // Move the file tokio::task::spawn_blocking(move || { std::fs::rename(&source_path, &target_path_clone) .map_err(|e| BackendError::from(format!("Failed to move file: {}", e))) }) .await .map_err(|e| BackendError::from(format!("Task error: {}", e)))?; Ok(target_path) } /// Ensure a folder exists /// /// # Arguments /// * `path` - Folder path /// /// # Returns /// * `Ok(())` - Folder exists or was created /// * `Err(BackendError)` - Error if folder creation failed pub async fn ensure_folder_exists(&self, path: &Path) -> Result<(), BackendError> { let path = path.to_path_buf(); tokio::task::spawn_blocking(move || { std::fs::create_dir_all(&path) .map_err(|e| BackendError::from(format!("Failed to create directory: {}", e))) }) .await .map_err(|e| BackendError::from(format!("Task error: {}", e)))? } /// Write an audit log entry /// /// # Arguments /// * `path` - Directory path for audit file /// * `event` - Event type (used for action tag) /// * `details` - Event details /// /// # Returns /// * `Ok(())` - Success /// * `Err(BackendError)` - Error if logging failed pub async fn write_audit_log( &self, path: &Path, event: &str, details: &str, ) -> Result<(), BackendError> { let _audit_path = path.join(".audit"); let directory = path.to_string_lossy().to_string(); // Parse the event type - clone directory for the match branches let directory_clone = directory.clone(); let action = match event { "directory_selected" => AuditAction::DirectorySelected { path: directory_clone }, "tag_file" => AuditAction::TagFile { file_path: details.to_string(), tag: "extra".to_string() // Default tag, could be parameterized }, "untag_file" => AuditAction::UntagFile { file_path: details.to_string(), tag: "extra".to_string() }, _ => AuditAction::DirectorySelected { path: directory.clone() }, }; let logger = AuditLogger::new(&directory); tokio::task::spawn_blocking(move || { logger.log_event(action) .map_err(|e| BackendError::from(format!("Failed to write audit log: {}", e))) }) .await .map_err(|e| BackendError::from(format!("Task error: {}", e)))? } /// Map files to Jellyfin naming convention /// /// # Arguments /// * `files` - Files to map /// * `show_name` - Show name /// * `season_number` - Season number /// * `tvdb_id` - Optional TVDB ID /// /// # Returns /// * `Ok(MappingResult)` - Mapping result with success/error counts /// * `Err(BackendError)` - Error if mapping failed pub async fn map_files( &self, files: &[MediaFile], show_name: &str, season_number: i32, tvdb_id: Option, ) -> Result { let mapper = FileMapper::new(); mapper .map_files(files, show_name, season_number, tvdb_id) .await .map_err(|e| BackendError::from(anyhow::anyhow!("Mapping failed: {}", e))) } /// Tag a file /// /// # Arguments /// * `file` - File to tag /// * `tag` - Tag to add /// /// # Returns /// * `Ok(MediaFile)` - Tagged file /// * `Err(BackendError)` - Error if tagging failed pub fn tag_file(&self, mut file: MediaFile, tag: &str) -> Result { file.add_tag(tag); Ok(file) } /// Untag a file /// /// # Arguments /// * `file` - File to untag /// * `tag` - Tag to remove /// /// # Returns /// * `Ok(MediaFile)` - Untagged file /// * `Err(BackendError)` - Error if untagging failed pub fn untag_file(&self, mut file: MediaFile, tag: &str) -> Result { file.remove_tag(tag); Ok(file) } /// Set up TVDB client /// /// # Arguments /// * `api_key` - TVDB API key /// /// # Returns /// * `Ok(TVDBClient)` - TVDB client /// * `Err(BackendError)` - Error if client creation failed pub fn setup_tvdb_client(&self, api_key: &str) -> Result { TVDBClient::new(api_key) .map_err(|e| BackendError::from(anyhow::anyhow!("Failed to create TVDB client: {}", e))) } /// Authenticate with TVDB /// /// # Arguments /// * `client` - TVDB client /// /// # Returns /// * `Ok(())` - Success /// * `Err(BackendError)` - Error if authentication failed pub async fn authenticate_tvdb(&self, client: &mut TVDBClient) -> Result<(), BackendError> { client .authenticate() .await .map_err(|e| BackendError::from(anyhow::anyhow!("TVDB authentication failed: {}", e))) } }