import Foundation /// Der komplette Weg einer einzelnen Datei: Dateiname parsen, bei Erfolg den /// Projekt-Unterordner anlegen, das unveränderte 9:16-Original ablegen und /// die 4:5-Fassung rendern. UI-frei und von `droptwo-cli` wie von der App /// gemeinsam genutzt, damit beide garantiert dasselbe tun. public enum ConversionPipeline { public struct Options { public var cropOptions: CropEngine.Options /// true = das unveränderte 9:16-Original wird zusätzlich in den /// Projektordner kopiert (README: "Dort landen beide Fassungen"). public var copyOriginal: Bool public init(cropOptions: CropEngine.Options = CropEngine.Options(), copyOriginal: Bool = true) { self.cropOptions = cropOptions self.copyOriginal = copyOriginal } } public enum Outcome { case skipped(fileName: String, reason: SkipReason) case rejected(fileName: String, reason: RejectionReason) case succeeded(Success) case failed(fileName: String, error: Error) } public struct Success { public let originalDestination: URL public let croppedDestination: URL public let sourceInfo: VideoInfo public let croppedInfo: VideoInfo } public static func process( sourceURL: URL, outputRootDirectory: URL, options: Options = Options(), progress: ((Double) -> Void)? = nil ) async -> Outcome { let fileName = sourceURL.lastPathComponent let result = FileNameParser.parse(fileName) switch result.outcome { case .skipped(let reason): return .skipped(fileName: fileName, reason: reason) case .rejected(let reason): return .rejected(fileName: fileName, reason: reason) case .parsed(let parsed): do { let success = try await convert(sourceURL: sourceURL, parsed: parsed, outputRootDirectory: outputRootDirectory, options: options, progress: progress) return .succeeded(success) } catch { return .failed(fileName: fileName, error: error) } } } private static func convert( sourceURL: URL, parsed: ParsedFileName, outputRootDirectory: URL, options: Options, progress: ((Double) -> Void)? ) async throws -> Success { let projectDirectory = outputRootDirectory.appendingPathComponent(parsed.projectName, isDirectory: true) try FileManager.default.createDirectory(at: projectDirectory, withIntermediateDirectories: true) var originalDestination = sourceURL if options.copyOriginal { let destination = projectDirectory.appendingPathComponent(parsed.originalFileName) if !FileManager.default.fileExists(atPath: destination.path) { try FileManager.default.copyItem(at: sourceURL, to: destination) } originalDestination = destination } let sourceInfo = try await VideoInspector.inspect(sourceURL) let croppedFileName = mp4FileName(from: parsed.targetFileName) let croppedDestination = projectDirectory.appendingPathComponent(croppedFileName) try await CropEngine.crop( sourceURL: sourceURL, destinationURL: croppedDestination, options: options.cropOptions, progress: progress ) let croppedInfo = try await VideoInspector.inspect(croppedDestination) return Success( originalDestination: originalDestination, croppedDestination: croppedDestination, sourceInfo: sourceInfo, croppedInfo: croppedInfo ) } /// Zielformat ist fest MP4 (siehe CropEngine) — die Endung aus dem /// geparsten Dateinamen wird dafür überschrieben, nicht übernommen. private static func mp4FileName(from fileName: String) -> String { URL(fileURLWithPath: fileName).deletingPathExtension().appendingPathExtension("mp4").lastPathComponent } }