Adds the new Rust-based UI (iced), backend service, shared Swift models, CI/CD workflows, build scripts, and project documentation.
295 lines
9.5 KiB
Rust
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)))
|
|
}
|
|
} |