You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
116 lines
4.8 KiB
116 lines
4.8 KiB
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
|
|
}
|
|
|
|
/// Berechnet nur, wohin die 4:5-Fassung geschrieben würde — ohne
|
|
/// irgendetwas anzufassen. `nil`, wenn der Dateiname nicht geparst werden
|
|
/// kann (dann übernimmt `process` die Skip/Reject-Meldung). Für die
|
|
/// Oberfläche gedacht: vor dem eigentlichen Lauf prüfen, ob eine Zieldatei
|
|
/// schon existiert, und bei Bedarf eine Rückfrage stellen.
|
|
public static func previewDestination(sourceURL: URL, outputRootDirectory: URL) -> URL? {
|
|
guard case .parsed(let parsed) = FileNameParser.parse(sourceURL.lastPathComponent).outcome else {
|
|
return nil
|
|
}
|
|
let projectDirectory = outputRootDirectory.appendingPathComponent(parsed.projectName, isDirectory: true)
|
|
return projectDirectory.appendingPathComponent(mp4FileName(from: parsed.targetFileName))
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|