|
|
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<JobResult> {
|
|
|
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..<min(concurrencyLimit, jobs.count) {
|
|
|
startNext()
|
|
|
}
|
|
|
|
|
|
while let result = await group.next() {
|
|
|
continuation.yield(result)
|
|
|
startNext()
|
|
|
}
|
|
|
}
|
|
|
continuation.finish()
|
|
|
}
|
|
|
continuation.onTermination = { _ in task.cancel() }
|
|
|
}
|
|
|
}
|
|
|
}
|