iPod Music conversion tools (macOS Only)
at trunk 211 lines 6.9 kB view raw
1use indicatif::{ParallelProgressIterator as _, ProgressIterator as _}; 2use rayon::iter::{IntoParallelIterator as _, ParallelIterator as _}; 3use std::{ 4 error::Error, ffi::OsStr, fs, os::unix::ffi::OsStrExt, path::PathBuf, thread::sleep, 5 time::Duration, 6}; 7use walkdir::{DirEntry, WalkDir}; 8 9use crate::{Rail, temp_file}; 10 11#[derive(clap::Args, Debug)] 12#[command(version, about, long_about = None)] 13pub struct ToAacArgs { 14 /// Replace source files with AAC conversions when done. 15 #[arg(long)] 16 pub(crate) replace: bool, 17 18 /// Attempt to copy metadata. Requires FFMPEG. 19 #[arg(long, default_value_t = false)] 20 pub(crate) no_metadata: bool, 21 #[arg(short, long, default_value_t = true)] 22 pub(crate) recursive: bool, 23 pub(crate) path: PathBuf, 24} 25 26fn is_flac(entry: &DirEntry) -> bool { 27 if entry.file_type().is_file() { 28 entry.path().extension() == Some(OsStr::new("flac")) 29 } else { 30 true 31 } 32} 33 34pub fn convert_to_aac(args: ToAacArgs) -> Rail { 35 let walker = WalkDir::new(&args.path); 36 37 let walker = if args.recursive { 38 walker.max_depth(1) 39 } else { 40 walker 41 }; 42 43 let files: Result<Vec<_>, _> = walker.into_iter().filter_entry(is_flac).collect(); 44 let files = files?; 45 let files_len = files.len() as u64; 46 47 if files.is_empty() { 48 return Err("No files".into()); 49 } 50 51 // SRC: https://hsu.cy/2025/07/itunesplus/ 52 fn convert(file: DirEntry, metadata: bool) -> Rail { 53 if file.file_type().is_dir() { 54 return Ok(()); 55 } 56 57 let flac_path = file.into_path(); 58 assert_eq!(flac_path.extension(), Some(OsStr::new("flac"))); 59 60 // Create output path by replacing .flac with .m4a 61 let output_path = flac_path.with_extension("m4a"); 62 63 // Skip if output already exists 64 if output_path.exists() { 65 eprintln!("Skipping {} (output already exists)", flac_path.display()); 66 return Ok(()); 67 } 68 69 // Create temporary WAV file path 70 let temp_wav = temp_file(Some(".wav")); 71 72 // Step 1: Convert FLAC to WAV using ffmpeg 73 let ffmpeg_output = std::process::Command::new("ffmpeg") 74 .arg("-i") 75 .arg(&flac_path) 76 .arg("-ac") 77 .arg("2") 78 .arg("-ar") 79 .arg("44100") 80 .arg(&temp_wav) 81 .arg("-y") // Overwrite without asking 82 .arg("-hide_banner") 83 .arg("-loglevel") 84 .arg("error") 85 .output() 86 .map_err(|e| format!("Failed to execute ffmpeg: {}", e))?; 87 88 if !ffmpeg_output.status.success() { 89 let stderr = String::from_utf8_lossy(&ffmpeg_output.stderr); 90 let _ = fs::remove_file(&temp_wav); 91 return Err(format!( 92 "ffmpeg conversion to WAV failed for {}: {}", 93 flac_path.display(), 94 stderr 95 ) 96 .into()); 97 } 98 99 // Verify temp WAV was created 100 if !temp_wav.exists() { 101 return Err( 102 format!("Temporary WAV file was not created: {}", temp_wav.display()).into(), 103 ); 104 } 105 106 // Step 2: Convert WAV to AAC using afconvert with iTunes Plus settings 107 let afconvert_output = std::process::Command::new("afconvert") 108 .arg(&temp_wav) 109 .arg("-d") 110 .arg("aac") 111 .arg("-f") 112 .arg("m4af") 113 .arg("-u") 114 .arg("pgcm") 115 .arg("2") 116 .arg("-b") 117 .arg("256000") 118 .arg("-q") 119 .arg("127") 120 .arg("-s") 121 .arg("2") 122 .arg(&output_path) 123 .output() 124 .map_err(|e| format!("Failed to execute afconvert: {}", e))?; 125 126 // Clean up temporary WAV file 127 temp_wav.close()?; 128 129 if !afconvert_output.status.success() { 130 let stderr = String::from_utf8_lossy(&afconvert_output.stderr); 131 let stdout = String::from_utf8_lossy(&afconvert_output.stdout); 132 return Err(format!( 133 "afconvert conversion to AAC failed for {}:\nstderr: {}\nstdout: {}", 134 flac_path.display(), 135 stderr, 136 stdout 137 ) 138 .into()); 139 } 140 141 // Verify output M4A was created 142 if !output_path.exists() { 143 return Err( 144 format!("Output M4A file was not created: {}", output_path.display()).into(), 145 ); 146 } 147 148 if metadata { 149 use metaflac::Tag as FlacTag; 150 use mp4ameta::Tag as Mp4Tag; 151 152 // Read FLAC metadata 153 let flac_tag = FlacTag::read_from_path(&flac_path) 154 .map_err(|e| format!("Failed to read FLAC metadata: {}", e))?; 155 156 // Open M4A for writing 157 let mut m4a_tag = Mp4Tag::read_from_path(&output_path) 158 .map_err(|e| format!("Failed to read M4A file: {}", e))?; 159 160 // Transfer common tags 161 if let Some(title) = flac_tag.get_vorbis("TITLE").and_then(|mut v| v.next()) { 162 m4a_tag.set_title(title); 163 } 164 if let Some(artist) = flac_tag.get_vorbis("ARTIST").and_then(|mut v| v.next()) { 165 m4a_tag.set_artist(artist); 166 } 167 if let Some(album) = flac_tag.get_vorbis("ALBUM").and_then(|mut v| v.next()) { 168 m4a_tag.set_album(album); 169 } 170 if let Some(album_artist) = flac_tag 171 .get_vorbis("ALBUMARTIST") 172 .and_then(|mut v| v.next()) 173 { 174 m4a_tag.set_album_artist(album_artist); 175 } 176 if let Some(genre) = flac_tag.get_vorbis("GENRE").and_then(|mut v| v.next()) { 177 m4a_tag.set_genre(genre); 178 } 179 if let Some(year) = flac_tag.get_vorbis("DATE").and_then(|mut v| v.next()) { 180 m4a_tag.set_year(year); 181 } 182 if let Some(track) = flac_tag 183 .get_vorbis("TRACKNUMBER") 184 .and_then(|mut v| v.next()) 185 && let Ok(track_num) = track.parse::<u16>() 186 { 187 m4a_tag.set_track_number(track_num); 188 } 189 if let Some(disc) = flac_tag.get_vorbis("DISCNUMBER").and_then(|mut v| v.next()) 190 && let Ok(disc_num) = disc.parse::<u16>() 191 { 192 m4a_tag.set_disc_number(disc_num); 193 } 194 195 // Write metadata to M4A 196 m4a_tag 197 .write_to_path(&output_path) 198 .map_err(|e| format!("Failed to write M4A metadata: {}", e))?; 199 } 200 201 Ok(()) 202 } 203 let res: Result<Vec<_>, _> = files 204 .into_par_iter() 205 .progress_count(files_len) 206 .map(|file| convert(file, !args.no_metadata)) 207 .collect(); 208 209 res.unwrap(); 210 Ok(()) 211}