its whats on the tin; culls raw photos
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: optimize import pipeline

+52 -43
+51 -42
cull/Services/PhotoImporter.swift
··· 13 13 let paired: Int // count of RAW+JPEG pairs found 14 14 } 15 15 16 + private static let exifDateFormatter: DateFormatter = { 17 + let f = DateFormatter() 18 + f.dateFormat = "yyyy:MM:dd HH:mm:ss" 19 + f.locale = Locale(identifier: "en_US_POSIX") 20 + return f 21 + }() 22 + 16 23 static func importFolder(_ url: URL) async throws -> ImportResult { 17 24 let resourceKeys: Set<URLResourceKey> = [.isRegularFileKey, .contentTypeKey] 18 25 guard let enumerator = FileManager.default.enumerator( ··· 60 67 } 61 68 } 62 69 63 - // Read EXIF dates + image metadata sequentially (header-only reads are fast, ~1ms each) 64 - for photo in photos { 65 - let dateURL = photo.url 66 - photo.captureDate = readCaptureDate(from: dateURL) 67 - readImageMetadata(from: photo.url, into: photo) 68 - if let pairedURL = photo.pairedURL { 69 - readPairedMetadata(from: pairedURL, into: photo) 70 + // Read EXIF dates + image metadata in parallel batches 71 + let formatter = exifDateFormatter 72 + for batchStart in stride(from: 0, to: photos.count, by: 16) { 73 + let batch = Array(photos[batchStart..<min(batchStart + 16, photos.count)]) 74 + await withTaskGroup(of: Void.self) { group in 75 + for photo in batch { 76 + group.addTask { 77 + readAllMetadata(photo: photo, formatter: formatter) 78 + } 79 + } 70 80 } 71 81 } 72 82 ··· 75 85 return ImportResult(photos: photos, paired: pairedCount) 76 86 } 77 87 78 - nonisolated static func readCaptureDate(from url: URL) -> Date? { 79 - guard let source = CGImageSourceCreateWithURL(url as CFURL, nil), 80 - let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any], 81 - let exif = properties[kCGImagePropertyExifDictionary as String] as? [String: Any], 82 - let dateString = exif[kCGImagePropertyExifDateTimeOriginal as String] as? String 83 - else { return nil } 84 - 85 - let formatter = DateFormatter() 86 - formatter.dateFormat = "yyyy:MM:dd HH:mm:ss" 87 - formatter.locale = Locale(identifier: "en_US_POSIX") 88 - return formatter.date(from: dateString) 89 - } 88 + /// Read all metadata from a single CGImageSource open — date, dimensions, file size, paired metadata 89 + nonisolated private static func readAllMetadata(photo: Photo, formatter: DateFormatter) { 90 + let url = photo.url 90 91 91 - nonisolated static func readPairedMetadata(from url: URL, into photo: Photo) { 92 + // File size from filesystem 92 93 if let attrs = try? FileManager.default.attributesOfItem(atPath: url.path), 93 94 let size = attrs[.size] as? Int64 { 94 - photo.pairedFileSize = size 95 + photo.fileSize = size 96 + } 97 + 98 + // Open image source once for date + dimensions 99 + if let source = CGImageSourceCreateWithURL(url as CFURL, nil), 100 + let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any] { 101 + // Dimensions 102 + if let width = properties[kCGImagePropertyPixelWidth as String] as? Int, 103 + let height = properties[kCGImagePropertyPixelHeight as String] as? Int { 104 + photo.pixelWidth = width 105 + photo.pixelHeight = height 106 + } 107 + // EXIF date 108 + if let exif = properties[kCGImagePropertyExifDictionary as String] as? [String: Any], 109 + let dateString = exif[kCGImagePropertyExifDateTimeOriginal as String] as? String { 110 + photo.captureDate = formatter.date(from: dateString) 111 + } 95 112 } 96 - guard let source = CGImageSourceCreateWithURL(url as CFURL, nil), 97 - let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any], 98 - let width = properties[kCGImagePropertyPixelWidth as String] as? Int, 99 - let height = properties[kCGImagePropertyPixelHeight as String] as? Int 100 - else { return } 101 - photo.pairedPixelWidth = width 102 - photo.pairedPixelHeight = height 103 - } 104 113 105 - nonisolated static func readImageMetadata(from url: URL, into photo: Photo) { 106 - // File size 107 - if let attrs = try? FileManager.default.attributesOfItem(atPath: url.path), 108 - let size = attrs[.size] as? Int64 { 109 - photo.fileSize = size 114 + // Paired file metadata 115 + if let pairedURL = photo.pairedURL { 116 + if let attrs = try? FileManager.default.attributesOfItem(atPath: pairedURL.path), 117 + let size = attrs[.size] as? Int64 { 118 + photo.pairedFileSize = size 119 + } 120 + if let source = CGImageSourceCreateWithURL(pairedURL as CFURL, nil), 121 + let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any], 122 + let width = properties[kCGImagePropertyPixelWidth as String] as? Int, 123 + let height = properties[kCGImagePropertyPixelHeight as String] as? Int { 124 + photo.pairedPixelWidth = width 125 + photo.pairedPixelHeight = height 126 + } 110 127 } 111 - // Pixel dimensions from image header (no full decode) 112 - guard let source = CGImageSourceCreateWithURL(url as CFURL, nil), 113 - let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any], 114 - let width = properties[kCGImagePropertyPixelWidth as String] as? Int, 115 - let height = properties[kCGImagePropertyPixelHeight as String] as? Int 116 - else { return } 117 - photo.pixelWidth = width 118 - photo.pixelHeight = height 119 128 } 120 129 121 130 static func isRAWExtension(_ ext: String) -> Bool {
+1 -1
cull/Services/ShotGrouper.swift
··· 22 22 var completed = 0.0 23 23 24 24 // Step 2: Generate feature prints for all photos (batched to report smooth progress) 25 - let fpWork: [(UUID, URL)] = photos.map { ($0.id, $0.pairedURL ?? $0.url) } 25 + let fpWork: [(UUID, URL)] = photos.map { ($0.id, $0.url) } 26 26 var featurePrintMap: [UUID: VNFeaturePrintObservation] = [:] 27 27 let batchSize = 8 28 28 for batchStart in stride(from: 0, to: fpWork.count, by: batchSize) {