Commit Diff


commit - 7c7d0a475c8515e0ce5aaa61c6f90780b27c418d
commit + 2f5ba04a772e26838b2731cff112a894e1f25fba
blob - d71e382216f13f4b7bbd57d20583ae2f733d8de6
blob + fe70eb45e9a9308bc0afb9bbaf059bbaa07b1df7
--- src/main.rs
+++ src/main.rs
@@ -110,11 +110,7 @@ async fn main() -> Result<()> {
         }
 
         if args.edit {
-            for file_path in all_files {
-                if let Err(e) = tagging::edit_file(&file_path).await {
-                    eprintln!("Failed to edit file {}: {e:?}", file_path.display());
-                }
-            }
+            tagging::edit_files(&all_files).await?;
         } else if args.show_tags {
             for file_path in all_files {
                 if let Err(e) = tagging::show_file_tags(&file_path) {
blob - a2aa5005fdd3974774b35e37c4c1920aece40e7b
blob + d0037a387c771c596a5c4e5ac7d564379f650d4a
--- src/tagging.rs
+++ src/tagging.rs
@@ -8,8 +8,9 @@ use lofty::probe::Probe;
 use lofty::tag::{ItemKey, Tag};
 use std::collections::HashMap;
 use std::fs::File;
+use std::io::Write;
 use std::path::Path;
-use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader};
+use tokio::io::{self, AsyncBufReadExt, BufReader};
 
 pub async fn process_files_batch(
     ctx: &AppContext,
@@ -165,12 +166,11 @@ fn parse_comma_separated(input: &str) -> Vec<String> {
 
 async fn prompt_field(
     stdin: &mut BufReader<tokio::io::Stdin>,
-    stdout: &mut tokio::io::Stdout,
     field_name: &str,
     current: &str,
 ) -> Result<String> {
     print!("{field_name} [{current}]: ");
-    stdout.flush().await?;
+    std::io::stdout().flush()?;
 
     let mut input = String::new();
     stdin.read_line(&mut input).await?;
@@ -233,44 +233,70 @@ fn save_with_backup(tagged_file: &mut TaggedFile, path
     Ok(())
 }
 
-pub async fn edit_file(path: &Path) -> Result<()> {
-    let abs_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
-
-    if !abs_path.exists() {
-        return Err(anyhow::anyhow!(
-            "File does not exist: {}",
-            abs_path.display()
-        ));
+pub async fn edit_files(paths: &[std::path::PathBuf]) -> Result<()> {
+    if paths.is_empty() {
+        println!("No audio files found.");
+        return Ok(());
     }
 
-    let (mut tagged_file, current_artist, current_album) = read_file_tags(&abs_path)?;
+    let mut files_by_release: HashMap<(String, String), Vec<std::path::PathBuf>> = HashMap::new();
 
-    let tag = tagged_file.primary_tag().context("No primary tag found")?;
-    let current_genre = tag
-        .get(ItemKey::Genre)
-        .and_then(|i| i.value().text())
-        .map(ToString::to_string)
-        .unwrap_or_default();
-    let current_label = tag
-        .get(ItemKey::Label)
-        .and_then(|i| i.value().text())
-        .map(ToString::to_string)
-        .unwrap_or_default();
+    for path in paths {
+        let abs_path = path.canonicalize().unwrap_or_else(|_| path.clone());
+        if !abs_path.exists() {
+            eprintln!("Skipping non-existent file: {}", path.display());
+            continue;
+        }
+        match read_file_tags(&abs_path) {
+            Ok((_, artist, album)) => {
+                files_by_release
+                    .entry((artist, album))
+                    .or_default()
+                    .push(abs_path);
+            }
+            Err(e) => eprintln!("Skipping {} - {e:?}", path.display()),
+        }
+    }
 
     let mut stdin = BufReader::new(io::stdin());
-    let mut stdout = io::stdout();
 
-    let artist = prompt_field(&mut stdin, &mut stdout, "Artist", &current_artist).await?;
-    let album = prompt_field(&mut stdin, &mut stdout, "Album", &current_album).await?;
-    let genre = prompt_field(&mut stdin, &mut stdout, "Genre", &current_genre).await?;
-    let label = prompt_field(&mut stdin, &mut stdout, "Label", &current_label).await?;
+    for ((artist, album), file_paths) in files_by_release {
+        let (tagged_file, _, _) = read_file_tags(&file_paths[0])?;
+        let tag = tagged_file.primary_tag().context("No primary tag found")?;
+        let current_genre = tag
+            .get(ItemKey::Genre)
+            .and_then(|i| i.value().text())
+            .map(ToString::to_string)
+            .unwrap_or_default();
+        let current_label = tag
+            .get(ItemKey::Label)
+            .and_then(|i| i.value().text())
+            .map(ToString::to_string)
+            .unwrap_or_default();
 
-    let tag = tagged_file
-        .primary_tag_mut()
-        .context("No primary tag found")?;
-    update_tag(tag, &artist, &album, &genre, &label);
+        println!(
+            "\nEditing: {artist} - {album} ({} file(s))",
+            file_paths.len()
+        );
 
-    save_with_backup(&mut tagged_file, &abs_path)
+        let new_artist = prompt_field(&mut stdin, "Artist", &artist).await?;
+        let new_album = prompt_field(&mut stdin, "Album", &album).await?;
+        let new_genre = prompt_field(&mut stdin, "Genre", &current_genre).await?;
+        let new_label = prompt_field(&mut stdin, "Label", &current_label).await?;
+
+        for file_path in &file_paths {
+            let (mut tagged_file, _, _) = read_file_tags(file_path)?;
+            let tag = tagged_file
+                .primary_tag_mut()
+                .context("No primary tag found")?;
+            update_tag(tag, &new_artist, &new_album, &new_genre, &new_label);
+            if let Err(e) = save_with_backup(&mut tagged_file, file_path) {
+                eprintln!("Failed to write {}: {e:?}", file_path.display());
+            }
+        }
+    }
+
+    Ok(())
 }
 
 fn read_file_tags(path: &Path) -> Result<(TaggedFile, String, String)> {