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)
|
||||
}
|
||||
}
|
||||
.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) {
|
||||
ForEach(DuplicateClipSelection.allCases) { selection in
|
||||
|
||||
@@ -10,6 +10,7 @@ enum OutputFormat: String, CaseIterable, Identifiable, Codable {
|
||||
|
||||
enum SortMode: String, CaseIterable, Identifiable, Codable {
|
||||
case filenameAscending = "Dateiname aufsteigend"
|
||||
case ageWeeksAscending = "Alter in Wochen aufsteigend"
|
||||
case modificationDateAscending = "Änderungsdatum aufsteigend"
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
@@ -113,7 +113,7 @@ final class VideoProcessor: ObservableObject {
|
||||
settings: effectiveSettings,
|
||||
outputFile: outputFile,
|
||||
targetDimensions: targetDimensions,
|
||||
inputClipCount: videos.count
|
||||
selectedVideos: videos
|
||||
)
|
||||
appendLog("Render-Protokoll: \(reportURL.path)")
|
||||
|
||||
@@ -204,6 +204,15 @@ final class VideoProcessor: ObservableObject {
|
||||
switch sortMode {
|
||||
case .filenameAscending:
|
||||
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:
|
||||
urls.sort {
|
||||
let left = (try? $0.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? .distantPast
|
||||
@@ -264,6 +273,18 @@ final class VideoProcessor: ObservableObject {
|
||||
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 {
|
||||
let sortedVariants = variants.sorted { left, right in
|
||||
let leftVersion = duplicateVersionNumber(for: left.url) ?? Int.max
|
||||
@@ -949,21 +970,36 @@ final class VideoProcessor: ObservableObject {
|
||||
settings: RenderSettings,
|
||||
outputFile: URL,
|
||||
targetDimensions: VideoDimensions,
|
||||
inputClipCount: Int
|
||||
selectedVideos: [VideoFile]
|
||||
) -> URL {
|
||||
let reportURL = outputFile
|
||||
.deletingPathExtension()
|
||||
.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 = [
|
||||
"GrowthLapse Render-Protokoll",
|
||||
"Datum: \(Date())",
|
||||
"Input-Ordner: \(settings.inputFolder?.path ?? "-")",
|
||||
"Output: \(outputFile.path)",
|
||||
"Input-Clips: \(inputClipCount)",
|
||||
"Input-Clips: \(selectedVideos.count)",
|
||||
"Zielauflösung: \(targetDimensions.width)x\(targetDimensions.height)",
|
||||
"Format: \(settings.outputFormat.rawValue)",
|
||||
"Encoder: \(settings.videoEncoder.rawValue)",
|
||||
"FPS: \(settings.fps)",
|
||||
"Sortierung: \(settings.sortMode.rawValue)",
|
||||
"Mehrfach-Versionen: \(settings.duplicateClipSelection.rawValue)",
|
||||
"Segmentlänge: \(settings.segmentLength)s",
|
||||
"Ziellänge pro Clip: \(settings.targetClipLength)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")",
|
||||
"Face Size Normalization: \(settings.faceNormalizationEnabled ? "an, Ziel \(Int(settings.targetFaceHeightRatio * 100))%" : "aus")",
|
||||
"",
|
||||
"Ausgewählte Clip-Reihenfolge:",
|
||||
selectedClipLines.joined(separator: "\n"),
|
||||
"",
|
||||
"Log:",
|
||||
logs.joined(separator: "\n")
|
||||
].joined(separator: "\n")
|
||||
|
||||
Reference in New Issue
Block a user