Commit Diff


commit - 782c6c972fe99b4e4e4b83810bc94e2c22bfbb17
commit + b2dc3e75bc4d1d2cb12c1febaec0cb16f51865cc
blob - 7aaff387edc36e8f4d4daf19a5ab2691650d281e
blob + 80bedbaac5f27a90291a80ad1692e992bedd7588
--- src/main.rs
+++ src/main.rs
@@ -20,20 +20,17 @@ use walkdir::WalkDir;
   Fetch metadata for an artist and album:
     $ hakuna --artist 'Djrum' --album 'Under Tangled Silence'
 
-  Preview tags for a single file:
+  Preview tags for a file:
     $ hakuna --read file.mp3
 
-  Write tags to a single file:
+  Write tags to a file:
     $ hakuna -d TOKEN --write file.mp3
 
-  Edit tags manually for a single file:
+  Edit tags manually:
     $ hakuna --edit file.mp3
 
-  Process all files in a directory:
-    $ hakuna -d TOKEN --write --all /path/to/music/
-
-  Edit all files in a directory with same values:
-    $ hakuna --edit --all /path/to/music/"
+  Process a directory:
+    $ hakuna -d TOKEN --write /path/to/music/"
 )]
 #[allow(clippy::struct_excessive_bools)]
 struct Args {
@@ -61,10 +58,6 @@ struct Args {
     #[arg(short, long, default_value_t = false)]
     edit: bool,
 
-    /// Process all files in directory(ies) at once
-    #[arg(short, long, default_value_t = false)]
-    all: bool,
-
     /// Discogs API token
     #[arg(short = 'd', long)]
     discogs_token: Option<String>,
@@ -95,7 +88,6 @@ fn collect_audio_files(path: &Path) -> Vec<std::path::
 
 #[tokio::main]
 async fn main() -> Result<()> {
-    // Show help if no arguments provided
     if env::args().len() == 1 {
         let _ = Args::parse_from(["hakuna", "--help"]);
         return Ok(());
@@ -118,17 +110,12 @@ async fn main() -> Result<()> {
         }
 
         if args.edit {
-            if args.all {
-                tagging::edit_files_batch(&all_files).await?;
-            } else {
-                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());
-                    }
+            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());
                 }
             }
         } else {
-            // Always use caching for read/write operations
             tagging::process_files_batch(&ctx, &all_files, args.read, args.write).await?;
         }
     } else if let (Some(artist), Some(album)) = (args.artist, args.album) {
blob - f6576b3e9f703f289c94a59677408cf4fb0900f6
blob + 26ca26f34bfcbe80759e3bd7f13f33f49d87f04f
--- src/tagging.rs
+++ src/tagging.rs
@@ -30,16 +30,16 @@ pub async fn process_files_batch(
     let mut files_by_release: HashMap<(String, String), Vec<std::path::PathBuf>> = HashMap::new();
 
     for path in paths {
-        let abs_path = path.clone();
+        let abs_path = path.canonicalize().unwrap_or_else(|_| path.clone());
         if !abs_path.exists() {
             eprintln!("Skipping non-existent file: {}", path.display());
             continue;
         }
 
-        match read_tagged_file(&abs_path) {
+        match read_file_tags(&abs_path) {
             Ok((_, artist, album)) => {
                 files_by_release
-                    .entry((artist.clone(), album.clone()))
+                    .entry((artist, album))
                     .or_default()
                     .push(abs_path);
             }
@@ -62,7 +62,6 @@ pub async fn process_files_batch(
     for ((artist, album), file_paths) in files_by_release {
         println!("\nFetching metadata for: {artist} - {album}");
 
-        // Fetch metadata once per release
         let result = match crate::metadata::fetch::process_query(ctx, &artist, &album).await {
             Ok(r) => r,
             Err(e) => {
@@ -76,13 +75,10 @@ pub async fn process_files_batch(
             print_proposed_tags(&result);
         }
 
-        // Apply to all files in this release group
         for file_path in file_paths {
             if write {
-                match write_single_file(&file_path, &result) {
-                    Ok(()) => {
-                        success_count += 1;
-                    }
+                match write_file_tags(&file_path, &result) {
+                    Ok(()) => success_count += 1,
                     Err(e) => {
                         eprintln!("  Failed to write {}: {e:?}", file_path.display());
                         error_count += 1;
@@ -101,14 +97,12 @@ pub async fn process_files_batch(
     Ok(())
 }
 
-fn write_single_file(path: &Path, metadata: &FetchedMetadata) -> Result<()> {
-    let abs_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
-
-    let file = File::open(&abs_path)
-        .with_context(|| format!("Failed to open file: {}", abs_path.display()))?;
+fn write_file_tags(path: &Path, metadata: &FetchedMetadata) -> Result<()> {
+    let file =
+        File::open(path).with_context(|| format!("Failed to open file: {}", path.display()))?;
     let mut probe = Probe::new(file);
 
-    if let Some(file_type) = abs_path
+    if let Some(file_type) = path
         .extension()
         .and_then(|e| e.to_str())
         .and_then(lofty::file::FileType::from_ext)
@@ -118,48 +112,14 @@ fn write_single_file(path: &Path, metadata: &FetchedMe
 
     let mut tagged_file = probe
         .read()
-        .with_context(|| format!("Failed to read tags from {}", abs_path.display()))?;
+        .with_context(|| format!("Failed to read tags from {}", path.display()))?;
 
     let tag = tagged_file
         .primary_tag_mut()
         .context("No primary tag found")?;
-    write_tags(tag, metadata);
+    apply_tags(tag, metadata);
 
-    let backup_path = abs_path.with_extension(format!(
-        "backup.{}",
-        abs_path
-            .extension()
-            .and_then(|s| s.to_str())
-            .unwrap_or("bak")
-    ));
-
-    std::fs::copy(&abs_path, &backup_path)
-        .with_context(|| format!("Failed to create backup: {}", backup_path.display()))?;
-
-    match tagged_file.save_to_path(&abs_path, WriteOptions::default()) {
-        Ok(()) => {
-            let _ = std::fs::remove_file(&backup_path);
-        }
-        Err(e) => {
-            if std::path::Path::exists(&backup_path) {
-                if let Err(restore_err) = std::fs::copy(&backup_path, &abs_path) {
-                    eprintln!("ERROR: Failed to restore from backup: {restore_err}");
-                    eprintln!(
-                        "WARNING: Original file may be corrupted. Backup preserved at: {}",
-                        backup_path.display()
-                    );
-                    return Err(
-                        anyhow::anyhow!("Failed to restore from backup: {restore_err}").context(e),
-                    );
-                }
-                eprintln!("Restored file from backup after write failure");
-                let _ = std::fs::remove_file(&backup_path);
-            }
-            return Err(anyhow::anyhow!("Failed to write tags to file").context(e));
-        }
-    }
-
-    Ok(())
+    save_with_backup(&mut tagged_file, path)
 }
 
 fn parse_comma_separated(input: &str) -> Vec<String> {
@@ -171,7 +131,7 @@ fn parse_comma_separated(input: &str) -> Vec<String> {
         .collect()
 }
 
-async fn prompt_edit_field(
+async fn prompt_field(
     stdin: &mut BufReader<tokio::io::Stdin>,
     stdout: &mut tokio::io::Stdout,
     field_name: &str,
@@ -193,55 +153,17 @@ async fn prompt_edit_field(
     }
 }
 
-async fn prompt_edit_field_batch(
-    stdin: &mut BufReader<tokio::io::Stdin>,
-    stdout: &mut tokio::io::Stdout,
-    field_name: &str,
-) -> Result<Option<String>> {
-    print!("{field_name}: ");
-    stdout.flush().await?;
+fn apply_tags(tag: &mut Tag, metadata: &FetchedMetadata) {
+    tag.remove_key(ItemKey::Genre);
+    tag.remove_key(ItemKey::Label);
 
-    let mut input = String::new();
-    stdin.read_line(&mut input).await?;
-    let input = input.trim();
-
-    if input.is_empty() {
-        Ok(None)
-    } else if input.eq_ignore_ascii_case("clear") {
-        Ok(Some(String::new()))
-    } else {
-        Ok(Some(input.to_string()))
+    if !metadata.genres.is_empty() {
+        tag.insert_text(ItemKey::Genre, metadata.genres.join("/"));
     }
-}
 
-fn update_tag(tag: &mut Tag, artist: &str, album: &str, genre: &str, label: &str) {
-    if artist.is_empty() {
-        tag.remove_key(ItemKey::TrackArtist);
-    } else {
-        tag.insert_text(ItemKey::TrackArtist, artist.to_string());
+    if let Some(label) = metadata.labels.first() {
+        tag.insert_text(ItemKey::Label, label.clone());
     }
-
-    if album.is_empty() {
-        tag.remove_key(ItemKey::AlbumTitle);
-    } else {
-        tag.insert_text(ItemKey::AlbumTitle, album.to_string());
-    }
-
-    if genre.is_empty() {
-        tag.remove_key(ItemKey::Genre);
-    } else {
-        let genres = parse_comma_separated(genre);
-        tag.insert_text(ItemKey::Genre, genres.join("/"));
-    }
-
-    if label.is_empty() {
-        tag.remove_key(ItemKey::Label);
-    } else {
-        let labels = parse_comma_separated(label);
-        if let Some(first) = labels.first() {
-            tag.insert_text(ItemKey::Label, first.clone());
-        }
-    }
 }
 
 fn save_with_backup(tagged_file: &mut TaggedFile, path: &Path) -> Result<()> {
@@ -281,13 +203,15 @@ fn save_with_backup(tagged_file: &mut TaggedFile, path
 
 pub async fn edit_file(path: &Path) -> Result<()> {
     let abs_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
-    let path_display = abs_path.display();
 
     if !abs_path.exists() {
-        return Err(anyhow::anyhow!("File does not exist: {path_display}"));
+        return Err(anyhow::anyhow!(
+            "File does not exist: {}",
+            abs_path.display()
+        ));
     }
 
-    let (mut tagged_file, current_artist, current_album) = read_tagged_file(&abs_path)?;
+    let (mut tagged_file, current_artist, current_album) = read_file_tags(&abs_path)?;
 
     let tag = tagged_file.primary_tag().context("No primary tag found")?;
     let current_genre = tag
@@ -304,119 +228,20 @@ pub async fn edit_file(path: &Path) -> Result<()> {
     let mut stdin = BufReader::new(io::stdin());
     let mut stdout = io::stdout();
 
-    let artist = prompt_edit_field(&mut stdin, &mut stdout, "Artist", &current_artist).await?;
-    let album = prompt_edit_field(&mut stdin, &mut stdout, "Album", &current_album).await?;
-    let genre = prompt_edit_field(&mut stdin, &mut stdout, "Genre", &current_genre).await?;
-    let label = prompt_edit_field(&mut stdin, &mut stdout, "Label", &current_label).await?;
+    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?;
 
     let tag = tagged_file
         .primary_tag_mut()
         .context("No primary tag found")?;
     update_tag(tag, &artist, &album, &genre, &label);
 
-    save_with_backup(&mut tagged_file, &abs_path)?;
-
-    Ok(())
+    save_with_backup(&mut tagged_file, &abs_path)
 }
 
-pub async fn edit_files_batch(paths: &[std::path::PathBuf]) -> Result<()> {
-    if paths.is_empty() {
-        println!("No audio files found to edit.");
-        return Ok(());
-    }
-
-    let mut stdin = BufReader::new(io::stdin());
-    let mut stdout = io::stdout();
-
-    let artist = prompt_edit_field_batch(&mut stdin, &mut stdout, "Artist").await?;
-    let album = prompt_edit_field_batch(&mut stdin, &mut stdout, "Album").await?;
-    let genre = prompt_edit_field_batch(&mut stdin, &mut stdout, "Genre").await?;
-    let label = prompt_edit_field_batch(&mut stdin, &mut stdout, "Label").await?;
-
-    let mut success_count = 0;
-
-    for path in paths {
-        let abs_path = path.clone();
-
-        if !abs_path.exists() {
-            eprintln!("Skipping non-existent file: {}", path.display());
-            continue;
-        }
-
-        match edit_apply_single(
-            &abs_path,
-            artist.as_deref(),
-            album.as_deref(),
-            genre.as_deref(),
-            label.as_deref(),
-        ) {
-            Ok(()) => {
-                success_count += 1;
-            }
-            Err(e) => {
-                eprintln!("Failed to edit {}: {e:?}", path.display());
-            }
-        }
-    }
-
-    println!("Edited {success_count} of {} files", paths.len());
-
-    Ok(())
-}
-
-fn edit_apply_single(
-    path: &Path,
-    artist: Option<&str>,
-    album: Option<&str>,
-    genre: Option<&str>,
-    label: Option<&str>,
-) -> Result<()> {
-    let (mut tagged_file, _, _) = read_tagged_file(path)?;
-    let tag = tagged_file
-        .primary_tag_mut()
-        .context("No primary tag found")?;
-
-    if let Some(val) = artist {
-        if val.is_empty() {
-            tag.remove_key(ItemKey::TrackArtist);
-        } else {
-            tag.insert_text(ItemKey::TrackArtist, val.to_string());
-        }
-    }
-
-    if let Some(val) = album {
-        if val.is_empty() {
-            tag.remove_key(ItemKey::AlbumTitle);
-        } else {
-            tag.insert_text(ItemKey::AlbumTitle, val.to_string());
-        }
-    }
-
-    if let Some(val) = genre {
-        if val.is_empty() {
-            tag.remove_key(ItemKey::Genre);
-        } else {
-            let genres = parse_comma_separated(val);
-            tag.insert_text(ItemKey::Genre, genres.join("/"));
-        }
-    }
-
-    if let Some(val) = label {
-        if val.is_empty() {
-            tag.remove_key(ItemKey::Label);
-        } else {
-            let labels = parse_comma_separated(val);
-            if let Some(first) = labels.first() {
-                tag.insert_text(ItemKey::Label, first.clone());
-            }
-        }
-    }
-
-    save_with_backup(&mut tagged_file, path)?;
-    Ok(())
-}
-
-fn read_tagged_file(path: &Path) -> Result<(TaggedFile, String, String)> {
+fn read_file_tags(path: &Path) -> Result<(TaggedFile, String, String)> {
     let file =
         File::open(path).with_context(|| format!("Failed to open file: {}", path.display()))?;
     let mut probe = Probe::new(file);
@@ -442,6 +267,36 @@ fn read_tagged_file(path: &Path) -> Result<(TaggedFile
     Ok((tagged_file, artist, album))
 }
 
+fn update_tag(tag: &mut Tag, artist: &str, album: &str, genre: &str, label: &str) {
+    if artist.is_empty() {
+        tag.remove_key(ItemKey::TrackArtist);
+    } else {
+        tag.insert_text(ItemKey::TrackArtist, artist.to_string());
+    }
+
+    if album.is_empty() {
+        tag.remove_key(ItemKey::AlbumTitle);
+    } else {
+        tag.insert_text(ItemKey::AlbumTitle, album.to_string());
+    }
+
+    if genre.is_empty() {
+        tag.remove_key(ItemKey::Genre);
+    } else {
+        let genres = parse_comma_separated(genre);
+        tag.insert_text(ItemKey::Genre, genres.join("/"));
+    }
+
+    if label.is_empty() {
+        tag.remove_key(ItemKey::Label);
+    } else {
+        let labels = parse_comma_separated(label);
+        if let Some(first) = labels.first() {
+            tag.insert_text(ItemKey::Label, first.clone());
+        }
+    }
+}
+
 fn print_proposed_tags(metadata: &FetchedMetadata) {
     println!("Proposed tags:");
     println!("  Genre: {}", metadata.genres.join("/"));
@@ -451,19 +306,6 @@ fn print_proposed_tags(metadata: &FetchedMetadata) {
     }
 }
 
-fn write_tags(tag: &mut Tag, metadata: &FetchedMetadata) {
-    tag.remove_key(ItemKey::Genre);
-    tag.remove_key(ItemKey::Label);
-
-    if !metadata.genres.is_empty() {
-        tag.insert_text(ItemKey::Genre, metadata.genres.join("/"));
-    }
-
-    if let Some(label) = metadata.labels.first() {
-        tag.insert_text(ItemKey::Label, label.clone());
-    }
-}
-
 pub fn print_metadata(result: &FetchedMetadata) {
     if result.genres.is_empty() {
         println!("Genres: (none)");