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.
drop.two/Sources/DropTwoCore/ConversionPipeline.swift

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
}
}

Powered by TurnKey Linux.