various: simplify code; work on security and performance

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I9a5114addcab5fbff430ab2b919b83466a6a6964
This commit is contained in:
raf 2026-02-02 17:32:11 +03:00
commit c4adc4e3e0
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
75 changed files with 12921 additions and 358 deletions

View file

@ -54,7 +54,7 @@ pub struct AppState {
pub total_media_count: u64,
pub server_url: String,
// Duplicates view
pub duplicate_groups: Vec<Vec<crate::client::MediaResponse>>,
pub duplicate_groups: Vec<crate::client::DuplicateGroupResponse>,
pub duplicates_selected: Option<usize>,
// Database view
pub database_stats: Option<Vec<(String, String)>>,
@ -249,16 +249,11 @@ fn handle_api_result(state: &mut AppState, result: ApiResult) {
}
}
ApiResult::Duplicates(groups) => {
let flat: Vec<Vec<crate::client::MediaResponse>> =
groups.into_iter().map(|g| g.items).collect();
state.duplicate_groups = flat;
if !state.duplicate_groups.is_empty() {
if !groups.is_empty() {
state.duplicates_selected = Some(0);
}
state.status_message = Some(format!(
"Found {} duplicate groups",
state.duplicate_groups.len()
));
state.status_message = Some(format!("Found {} duplicate groups", groups.len()));
state.duplicate_groups = groups;
}
ApiResult::DatabaseStats(stats) => {
state.database_stats = Some(vec![
@ -617,6 +612,13 @@ async fn handle_action(
}
}
}
// Also fetch background jobs info
match client.list_jobs().await {
Ok(jobs) => {
tracing::debug!("Found {} background jobs", jobs.len());
}
Err(e) => tracing::warn!("Failed to list jobs: {}", e),
}
});
}
Action::QueueView => {
@ -1024,6 +1026,134 @@ async fn handle_action(
"?: Help q: Quit /: Search i: Import o: Open t: Tags c: Collections a: Audit s: Scan S: Settings r: Refresh Home/End: Top/Bottom".into()
);
}
Action::Edit => {
if state.current_view == View::Detail
&& let Some(ref media) = state.selected_media {
// Populate edit fields from selected media
state.edit_title = media.title.clone().unwrap_or_default();
state.edit_artist = media.artist.clone().unwrap_or_default();
state.edit_album = media.album.clone().unwrap_or_default();
state.edit_genre = media.genre.clone().unwrap_or_default();
state.edit_year = media.year.map(|y| y.to_string()).unwrap_or_default();
state.edit_description = media.description.clone().unwrap_or_default();
state.edit_field_index = Some(0);
state.input_mode = true;
state.current_view = View::MetadataEdit;
}
}
Action::Vacuum => {
if state.current_view == View::Database {
state.status_message = Some("Vacuuming database...".to_string());
let client = client.clone();
let tx = event_sender.clone();
tokio::spawn(async move {
match client.vacuum_database().await {
Ok(()) => {
tracing::info!("Database vacuum completed");
// Refresh stats after vacuum
if let Ok(stats) = client.database_stats().await {
let _ =
tx.send(AppEvent::ApiResult(ApiResult::DatabaseStats(stats)));
}
}
Err(e) => {
tracing::error!("Vacuum failed: {}", e);
let _ = tx.send(AppEvent::ApiResult(ApiResult::Error(format!(
"Vacuum failed: {e}"
))));
}
}
});
}
}
Action::Toggle => {
if state.current_view == View::Tasks
&& let Some(idx) = state.scheduled_tasks_selected
&& let Some(task) = state.scheduled_tasks.get(idx) {
let task_id = task.id.clone();
let client = client.clone();
let tx = event_sender.clone();
tokio::spawn(async move {
match client.toggle_scheduled_task(&task_id).await {
Ok(()) => {
// Refresh tasks list
if let Ok(tasks) = client.list_scheduled_tasks().await {
let _ = tx.send(AppEvent::ApiResult(
ApiResult::ScheduledTasks(tasks),
));
}
}
Err(e) => {
tracing::error!("Failed to toggle task: {}", e);
let _ = tx.send(AppEvent::ApiResult(ApiResult::Error(
format!("Toggle task failed: {e}"),
)));
}
}
});
}
}
Action::RunNow => {
if state.current_view == View::Tasks
&& let Some(idx) = state.scheduled_tasks_selected
&& let Some(task) = state.scheduled_tasks.get(idx) {
let task_id = task.id.clone();
let task_name = task.name.clone();
state.status_message = Some(format!("Running task: {task_name}..."));
let client = client.clone();
let tx = event_sender.clone();
tokio::spawn(async move {
match client.run_task_now(&task_id).await {
Ok(()) => {
// Refresh tasks list
if let Ok(tasks) = client.list_scheduled_tasks().await {
let _ = tx.send(AppEvent::ApiResult(
ApiResult::ScheduledTasks(tasks),
));
}
}
Err(e) => {
tracing::error!("Failed to run task: {}", e);
let _ = tx.send(AppEvent::ApiResult(ApiResult::Error(
format!("Run task failed: {e}"),
)));
}
}
});
}
}
Action::Save => {
if state.current_view == View::MetadataEdit
&& let Some(ref media) = state.selected_media {
let updates = serde_json::json!({
"title": if state.edit_title.is_empty() { serde_json::Value::Null } else { serde_json::Value::String(state.edit_title.clone()) },
"artist": if state.edit_artist.is_empty() { serde_json::Value::Null } else { serde_json::Value::String(state.edit_artist.clone()) },
"album": if state.edit_album.is_empty() { serde_json::Value::Null } else { serde_json::Value::String(state.edit_album.clone()) },
"genre": if state.edit_genre.is_empty() { serde_json::Value::Null } else { serde_json::Value::String(state.edit_genre.clone()) },
"year": state.edit_year.parse::<i32>().ok(),
"description": if state.edit_description.is_empty() { serde_json::Value::Null } else { serde_json::Value::String(state.edit_description.clone()) },
});
let media_id = media.id.clone();
let client = client.clone();
let tx = event_sender.clone();
state.status_message = Some("Saving...".to_string());
tokio::spawn(async move {
match client.update_media(&media_id, updates).await {
Ok(_) => {
let _ = tx.send(AppEvent::ApiResult(ApiResult::MediaUpdated));
}
Err(e) => {
tracing::error!("Failed to update media: {}", e);
let _ = tx.send(AppEvent::ApiResult(ApiResult::Error(format!(
"Update failed: {e}"
))));
}
}
});
state.input_mode = false;
state.current_view = View::Detail;
}
}
Action::NavigateLeft | Action::NavigateRight | Action::None => {}
}
}