Add MP3Gain normalization tool

This commit is contained in:
Mikei386
2026-06-07 13:27:20 +02:00
parent ce47ceb176
commit b0d0e14f3a
3 changed files with 130 additions and 1 deletions
+99
View File
@@ -425,6 +425,93 @@ struct SDCardCleaner {
}
}
struct MP3GainNormalizer {
private let fileManager: FileManager
init(fileManager: FileManager) {
self.fileManager = fileManager
}
func normalize(rootURL: URL) -> [ExportMessage] {
guard let mp3gainURL = mp3gainExecutableURL() else {
return [.error("mp3gain wurde nicht gefunden. Erwartet z. B. /opt/homebrew/bin/mp3gain.")]
}
do {
let mp3Files = try MP3FileFinder(fileManager: fileManager).mp3Files(in: rootURL)
guard !mp3Files.isEmpty else {
return [.warning("Keine MP3-Dateien für MP3Gain gefunden.")]
}
var messages: [ExportMessage] = []
var processed = 0
for batch in mp3Files.chunked(into: 50) {
let result = runMP3Gain(executableURL: mp3gainURL, files: batch)
messages.append(contentsOf: result.messages)
if result.success {
processed += batch.count
}
}
messages.insert(.info("MP3Gain abgeschlossen: \(processed) von \(mp3Files.count) Datei(en) verarbeitet."), at: 0)
return messages
} catch {
return [.error("MP3Gain-Dateisuche fehlgeschlagen: \(error.localizedDescription)")]
}
}
private func runMP3Gain(executableURL: URL, files: [URL]) -> (success: Bool, messages: [ExportMessage]) {
let process = Process()
process.executableURL = executableURL
process.arguments = ["-r", "-k", "-d", "4"] + files.map { $0.path(percentEncoded: false) }
let outputPipe = Pipe()
let errorPipe = Pipe()
process.standardOutput = outputPipe
process.standardError = errorPipe
do {
try process.run()
process.waitUntilExit()
} catch {
return (false, [.error("mp3gain konnte nicht gestartet werden: \(error.localizedDescription)")])
}
let output = outputPipe.readString()
let errorOutput = errorPipe.readString()
var messages: [ExportMessage] = []
if process.terminationStatus == 0 {
messages.append(.info("MP3Gain Batch verarbeitet: \(files.count) Datei(en)"))
} else {
messages.append(.error("MP3Gain beendet mit Code \(process.terminationStatus)."))
}
if !output.isEmpty {
messages.append(.info(output))
}
if !errorOutput.isEmpty {
messages.append(process.terminationStatus == 0 ? .warning(errorOutput) : .error(errorOutput))
}
return (process.terminationStatus == 0, messages)
}
private func mp3gainExecutableURL() -> URL? {
let candidates = [
"/opt/homebrew/bin/mp3gain",
"/usr/local/bin/mp3gain",
"/usr/bin/mp3gain"
]
return candidates
.map(URL.init(fileURLWithPath:))
.first { fileManager.isExecutableFile(atPath: $0.path(percentEncoded: false)) }
}
}
struct SDCardValidator {
private let fileManager: FileManager
@@ -1253,6 +1340,18 @@ private extension Array where Element == ExportMessage {
}
}
private extension Array {
func chunked(into size: Int) -> [[Element]] {
guard size > 0 else {
return [self]
}
return stride(from: 0, to: count, by: size).map {
Array(self[$0..<Swift.min($0 + size, count)])
}
}
}
private extension Array where Element == URL {
func sortedByLocalizedName() -> [URL] {
sorted {