Add age-based sorting and clip report manifest
This commit is contained in:
@@ -106,7 +106,7 @@ struct ContentView: View {
|
|||||||
Text(sortMode.rawValue).tag(sortMode)
|
Text(sortMode.rawValue).tag(sortMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.help("Bestimmt die chronologische Reihenfolge der Quellvideos: nach Dateiname oder Änderungsdatum.")
|
.help("Bestimmt die chronologische Reihenfolge der Quellvideos. Wenn Dateinamen wie '118 Wochen alt' enthalten sind, nutze 'Alter in Wochen aufsteigend'.")
|
||||||
|
|
||||||
Picker("Mehrfach-Versionen", selection: $settings.duplicateClipSelection) {
|
Picker("Mehrfach-Versionen", selection: $settings.duplicateClipSelection) {
|
||||||
ForEach(DuplicateClipSelection.allCases) { selection in
|
ForEach(DuplicateClipSelection.allCases) { selection in
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ enum OutputFormat: String, CaseIterable, Identifiable, Codable {
|
|||||||
|
|
||||||
enum SortMode: String, CaseIterable, Identifiable, Codable {
|
enum SortMode: String, CaseIterable, Identifiable, Codable {
|
||||||
case filenameAscending = "Dateiname aufsteigend"
|
case filenameAscending = "Dateiname aufsteigend"
|
||||||
|
case ageWeeksAscending = "Alter in Wochen aufsteigend"
|
||||||
case modificationDateAscending = "Änderungsdatum aufsteigend"
|
case modificationDateAscending = "Änderungsdatum aufsteigend"
|
||||||
|
|
||||||
var id: String { rawValue }
|
var id: String { rawValue }
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ final class VideoProcessor: ObservableObject {
|
|||||||
settings: effectiveSettings,
|
settings: effectiveSettings,
|
||||||
outputFile: outputFile,
|
outputFile: outputFile,
|
||||||
targetDimensions: targetDimensions,
|
targetDimensions: targetDimensions,
|
||||||
inputClipCount: videos.count
|
selectedVideos: videos
|
||||||
)
|
)
|
||||||
appendLog("Render-Protokoll: \(reportURL.path)")
|
appendLog("Render-Protokoll: \(reportURL.path)")
|
||||||
|
|
||||||
@@ -204,6 +204,15 @@ final class VideoProcessor: ObservableObject {
|
|||||||
switch sortMode {
|
switch sortMode {
|
||||||
case .filenameAscending:
|
case .filenameAscending:
|
||||||
urls.sort { $0.lastPathComponent.localizedStandardCompare($1.lastPathComponent) == .orderedAscending }
|
urls.sort { $0.lastPathComponent.localizedStandardCompare($1.lastPathComponent) == .orderedAscending }
|
||||||
|
case .ageWeeksAscending:
|
||||||
|
urls.sort { left, right in
|
||||||
|
let leftAge = ageInWeeks(from: left)
|
||||||
|
let rightAge = ageInWeeks(from: right)
|
||||||
|
if leftAge != rightAge {
|
||||||
|
return (leftAge ?? Int.max) < (rightAge ?? Int.max)
|
||||||
|
}
|
||||||
|
return left.lastPathComponent.localizedStandardCompare(right.lastPathComponent) == .orderedAscending
|
||||||
|
}
|
||||||
case .modificationDateAscending:
|
case .modificationDateAscending:
|
||||||
urls.sort {
|
urls.sort {
|
||||||
let left = (try? $0.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? .distantPast
|
let left = (try? $0.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? .distantPast
|
||||||
@@ -264,6 +273,18 @@ final class VideoProcessor: ObservableObject {
|
|||||||
return selected
|
return selected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func ageInWeeks(from url: URL) -> Int? {
|
||||||
|
let name = url.deletingPathExtension().lastPathComponent
|
||||||
|
let pattern = #"(\d{1,4})\s*Wochen\s+alt"#
|
||||||
|
guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]),
|
||||||
|
let match = regex.firstMatch(in: name, range: NSRange(name.startIndex..., in: name)),
|
||||||
|
match.numberOfRanges == 2,
|
||||||
|
let range = Range(match.range(at: 1), in: name) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return Int(name[range])
|
||||||
|
}
|
||||||
|
|
||||||
private func chooseDuplicateVariant(from variants: [VideoFile], selection: DuplicateClipSelection) -> VideoFile {
|
private func chooseDuplicateVariant(from variants: [VideoFile], selection: DuplicateClipSelection) -> VideoFile {
|
||||||
let sortedVariants = variants.sorted { left, right in
|
let sortedVariants = variants.sorted { left, right in
|
||||||
let leftVersion = duplicateVersionNumber(for: left.url) ?? Int.max
|
let leftVersion = duplicateVersionNumber(for: left.url) ?? Int.max
|
||||||
@@ -949,21 +970,36 @@ final class VideoProcessor: ObservableObject {
|
|||||||
settings: RenderSettings,
|
settings: RenderSettings,
|
||||||
outputFile: URL,
|
outputFile: URL,
|
||||||
targetDimensions: VideoDimensions,
|
targetDimensions: VideoDimensions,
|
||||||
inputClipCount: Int
|
selectedVideos: [VideoFile]
|
||||||
) -> URL {
|
) -> URL {
|
||||||
let reportURL = outputFile
|
let reportURL = outputFile
|
||||||
.deletingPathExtension()
|
.deletingPathExtension()
|
||||||
.appendingPathExtension("growthlapse-report.txt")
|
.appendingPathExtension("growthlapse-report.txt")
|
||||||
|
let selectedClipLines = selectedVideos.enumerated().map { index, video in
|
||||||
|
let age = ageInWeeks(from: video.url).map { ", \($0) Wochen" } ?? ""
|
||||||
|
return String(
|
||||||
|
format: "%03d. %@ - %@, %dx%d%@",
|
||||||
|
index + 1,
|
||||||
|
video.displayName,
|
||||||
|
formatSeconds(video.duration),
|
||||||
|
video.width,
|
||||||
|
video.height,
|
||||||
|
age
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
let summary = [
|
let summary = [
|
||||||
"GrowthLapse Render-Protokoll",
|
"GrowthLapse Render-Protokoll",
|
||||||
"Datum: \(Date())",
|
"Datum: \(Date())",
|
||||||
|
"Input-Ordner: \(settings.inputFolder?.path ?? "-")",
|
||||||
"Output: \(outputFile.path)",
|
"Output: \(outputFile.path)",
|
||||||
"Input-Clips: \(inputClipCount)",
|
"Input-Clips: \(selectedVideos.count)",
|
||||||
"Zielauflösung: \(targetDimensions.width)x\(targetDimensions.height)",
|
"Zielauflösung: \(targetDimensions.width)x\(targetDimensions.height)",
|
||||||
"Format: \(settings.outputFormat.rawValue)",
|
"Format: \(settings.outputFormat.rawValue)",
|
||||||
"Encoder: \(settings.videoEncoder.rawValue)",
|
"Encoder: \(settings.videoEncoder.rawValue)",
|
||||||
"FPS: \(settings.fps)",
|
"FPS: \(settings.fps)",
|
||||||
|
"Sortierung: \(settings.sortMode.rawValue)",
|
||||||
|
"Mehrfach-Versionen: \(settings.duplicateClipSelection.rawValue)",
|
||||||
"Segmentlänge: \(settings.segmentLength)s",
|
"Segmentlänge: \(settings.segmentLength)s",
|
||||||
"Ziellänge pro Clip: \(settings.targetClipLength)s",
|
"Ziellänge pro Clip: \(settings.targetClipLength)s",
|
||||||
"Übergangslänge: \(settings.transitionLength)s",
|
"Übergangslänge: \(settings.transitionLength)s",
|
||||||
@@ -974,6 +1010,9 @@ final class VideoProcessor: ObservableObject {
|
|||||||
"Stabilisierung Hintergrund-Anker: \(settings.stabilizationAnchorEnabled ? "an, X \(Int(settings.stabilizationAnchorX * 100))%, Y \(Int(settings.stabilizationAnchorY * 100))%, Bereich \(Int(settings.stabilizationAnchorSize * 100))%" : "aus")",
|
"Stabilisierung Hintergrund-Anker: \(settings.stabilizationAnchorEnabled ? "an, X \(Int(settings.stabilizationAnchorX * 100))%, Y \(Int(settings.stabilizationAnchorY * 100))%, Bereich \(Int(settings.stabilizationAnchorSize * 100))%" : "aus")",
|
||||||
"Face Size Normalization: \(settings.faceNormalizationEnabled ? "an, Ziel \(Int(settings.targetFaceHeightRatio * 100))%" : "aus")",
|
"Face Size Normalization: \(settings.faceNormalizationEnabled ? "an, Ziel \(Int(settings.targetFaceHeightRatio * 100))%" : "aus")",
|
||||||
"",
|
"",
|
||||||
|
"Ausgewählte Clip-Reihenfolge:",
|
||||||
|
selectedClipLines.joined(separator: "\n"),
|
||||||
|
"",
|
||||||
"Log:",
|
"Log:",
|
||||||
logs.joined(separator: "\n")
|
logs.joined(separator: "\n")
|
||||||
].joined(separator: "\n")
|
].joined(separator: "\n")
|
||||||
|
|||||||
Reference in New Issue
Block a user