iPod Music conversion tools (macOS Only)
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}