MovieMapper/backend/src/backend.rs
Jarian Cottingham 68eb4d9f43 feat: Add Rust UI and backend implementation with project infrastructure
Adds the new Rust-based UI (iced), backend service, shared Swift models,
CI/CD workflows, build scripts, and project documentation.
2026-08-19 21:22:08 -05:00

295 lines
9.5 KiB
Rust

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<MediaFile>,
/// Currently selected directory
pub current_directory: Option<PathBuf>,
/// Navigation stack for back button
pub navigation_stack: Vec<PathBuf>,
/// Tagged files
pub tagged_files: Vec<MediaFile>,
/// TVDB API client (if configured)
pub tvdb_client: Option<TVDBClientState>,
}
/// TVDB client state
#[derive(Debug, Clone)]
pub struct TVDBClientState {
/// API key
pub api_key: String,
/// Auth token (if authenticated)
pub auth_token: Option<String>,
/// 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<MediaFile>)` - 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<Vec<MediaFile>, 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<PathBuf, BackendError> {
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<i64>,
) -> Result<movie_mapper::service::file_mapper::MappingResult, BackendError> {
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<MediaFile, BackendError> {
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<MediaFile, BackendError> {
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, BackendError> {
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)))
}
}