import Foundation /// Verarbeitet mehrere Dateien über `ConversionPipeline`, begrenzt auf eine /// feste Anzahl gleichzeitiger Exporte (README: "1–2 parallele Exports"). /// Ergebnisse kommen fortlaufend rein, sobald ein Job fertig ist, nicht erst /// am Ende des gesamten Batches — die UI kann so live mitschreiben. public enum JobQueue { public struct Job: Sendable { public let id: UUID public let sourceURL: URL /// Ausschnitt nur für diese Datei. nil = der aus den gemeinsamen /// `options` des Batches. public let cropPlan: CropPlan? /// Format anhängen, falls es im Namen fehlt — nur für diese Datei. /// nil = wie in den gemeinsamen `options`. public let appendFormatWhenMissing: Bool? public init(id: UUID = UUID(), sourceURL: URL, cropPlan: CropPlan? = nil, appendFormatWhenMissing: Bool? = nil) { self.id = id self.sourceURL = sourceURL self.cropPlan = cropPlan self.appendFormatWhenMissing = appendFormatWhenMissing } } public struct JobResult: Sendable { public let job: Job public let outcome: ConversionPipeline.Outcome } public static func run( jobs: [Job], outputRootDirectory: URL, options: ConversionPipeline.Options = ConversionPipeline.Options(), maxConcurrent: Int = 2, progress: (@Sendable (Job, Double) -> Void)? = nil ) -> AsyncStream { AsyncStream { continuation in let task = Task { await withTaskGroup(of: JobResult.self) { group in var iterator = jobs.makeIterator() func startNext() { guard let job = iterator.next() else { return } var jobOptions = options if let cropPlan = job.cropPlan { jobOptions.cropOptions.cropPlan = cropPlan } if let appendFormatWhenMissing = job.appendFormatWhenMissing { jobOptions.appendFormatWhenMissing = appendFormatWhenMissing } group.addTask { [jobOptions] in let outcome = await ConversionPipeline.process( sourceURL: job.sourceURL, outputRootDirectory: outputRootDirectory, options: jobOptions ) { value in progress?(job, value) } return JobResult(job: job, outcome: outcome) } } let concurrencyLimit = max(1, maxConcurrent) for _ in 0..