use indicatif::{ParallelProgressIterator as _, ProgressIterator as _}; use rayon::iter::{IntoParallelIterator as _, ParallelIterator as _}; use std::{ error::Error, ffi::OsStr, fs, os::unix::ffi::OsStrExt, path::PathBuf, thread::sleep, time::Duration, }; use walkdir::{DirEntry, WalkDir}; use crate::{Rail, temp_file}; #[derive(clap::Args, Debug)] #[command(version, about, long_about = None)] pub struct ToAacArgs { /// Replace source files with AAC conversions when done. #[arg(long)] pub(crate) replace: bool, /// Attempt to copy metadata. Requires FFMPEG. #[arg(long, default_value_t = false)] pub(crate) no_metadata: bool, #[arg(short, long, default_value_t = true)] pub(crate) recursive: bool, pub(crate) path: PathBuf, } fn is_flac(entry: &DirEntry) -> bool { if entry.file_type().is_file() { entry.path().extension() == Some(OsStr::new("flac")) } else { true } } pub fn convert_to_aac(args: ToAacArgs) -> Rail { let walker = WalkDir::new(&args.path); let walker = if args.recursive { walker.max_depth(1) } else { walker }; let files: Result, _> = walker.into_iter().filter_entry(is_flac).collect(); let files = files?; let files_len = files.len() as u64; if files.is_empty() { return Err("No files".into()); } // SRC: https://hsu.cy/2025/07/itunesplus/ fn convert(file: DirEntry, metadata: bool) -> Rail { if file.file_type().is_dir() { return Ok(()); } let flac_path = file.into_path(); assert_eq!(flac_path.extension(), Some(OsStr::new("flac"))); // Create output path by replacing .flac with .m4a let output_path = flac_path.with_extension("m4a"); // Skip if output already exists if output_path.exists() { eprintln!("Skipping {} (output already exists)", flac_path.display()); return Ok(()); } // Create temporary WAV file path let temp_wav = temp_file(Some(".wav")); // Step 1: Convert FLAC to WAV using ffmpeg let ffmpeg_output = std::process::Command::new("ffmpeg") .arg("-i") .arg(&flac_path) .arg("-ac") .arg("2") .arg("-ar") .arg("44100") .arg(&temp_wav) .arg("-y") // Overwrite without asking .arg("-hide_banner") .arg("-loglevel") .arg("error") .output() .map_err(|e| format!("Failed to execute ffmpeg: {}", e))?; if !ffmpeg_output.status.success() { let stderr = String::from_utf8_lossy(&ffmpeg_output.stderr); let _ = fs::remove_file(&temp_wav); return Err(format!( "ffmpeg conversion to WAV failed for {}: {}", flac_path.display(), stderr ) .into()); } // Verify temp WAV was created if !temp_wav.exists() { return Err( format!("Temporary WAV file was not created: {}", temp_wav.display()).into(), ); } // Step 2: Convert WAV to AAC using afconvert with iTunes Plus settings let afconvert_output = std::process::Command::new("afconvert") .arg(&temp_wav) .arg("-d") .arg("aac") .arg("-f") .arg("m4af") .arg("-u") .arg("pgcm") .arg("2") .arg("-b") .arg("256000") .arg("-q") .arg("127") .arg("-s") .arg("2") .arg(&output_path) .output() .map_err(|e| format!("Failed to execute afconvert: {}", e))?; // Clean up temporary WAV file temp_wav.close()?; if !afconvert_output.status.success() { let stderr = String::from_utf8_lossy(&afconvert_output.stderr); let stdout = String::from_utf8_lossy(&afconvert_output.stdout); return Err(format!( "afconvert conversion to AAC failed for {}:\nstderr: {}\nstdout: {}", flac_path.display(), stderr, stdout ) .into()); } // Verify output M4A was created if !output_path.exists() { return Err( format!("Output M4A file was not created: {}", output_path.display()).into(), ); } if metadata { use metaflac::Tag as FlacTag; use mp4ameta::Tag as Mp4Tag; // Read FLAC metadata let flac_tag = FlacTag::read_from_path(&flac_path) .map_err(|e| format!("Failed to read FLAC metadata: {}", e))?; // Open M4A for writing let mut m4a_tag = Mp4Tag::read_from_path(&output_path) .map_err(|e| format!("Failed to read M4A file: {}", e))?; // Transfer common tags if let Some(title) = flac_tag.get_vorbis("TITLE").and_then(|mut v| v.next()) { m4a_tag.set_title(title); } if let Some(artist) = flac_tag.get_vorbis("ARTIST").and_then(|mut v| v.next()) { m4a_tag.set_artist(artist); } if let Some(album) = flac_tag.get_vorbis("ALBUM").and_then(|mut v| v.next()) { m4a_tag.set_album(album); } if let Some(album_artist) = flac_tag .get_vorbis("ALBUMARTIST") .and_then(|mut v| v.next()) { m4a_tag.set_album_artist(album_artist); } if let Some(genre) = flac_tag.get_vorbis("GENRE").and_then(|mut v| v.next()) { m4a_tag.set_genre(genre); } if let Some(year) = flac_tag.get_vorbis("DATE").and_then(|mut v| v.next()) { m4a_tag.set_year(year); } if let Some(track) = flac_tag .get_vorbis("TRACKNUMBER") .and_then(|mut v| v.next()) && let Ok(track_num) = track.parse::() { m4a_tag.set_track_number(track_num); } if let Some(disc) = flac_tag.get_vorbis("DISCNUMBER").and_then(|mut v| v.next()) && let Ok(disc_num) = disc.parse::() { m4a_tag.set_disc_number(disc_num); } // Write metadata to M4A m4a_tag .write_to_path(&output_path) .map_err(|e| format!("Failed to write M4A metadata: {}", e))?; } Ok(()) } let res: Result, _> = files .into_par_iter() .progress_count(files_len) .map(|file| convert(file, !args.no_metadata)) .collect(); res.unwrap(); Ok(()) }