├── photoz ├── Bridging-Header.h ├── Extensions.swift ├── main.swift └── Methods.swift ├── photoz.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcuserdata │ └── thall.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── project.pbxproj ├── README.md ├── LICENSE.md └── .gitignore /photoz/Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Bridging-Header.h 3 | // photoz 4 | // 5 | // Created by Tyler Hall on 4/4/20. 6 | // Copyright © 2020 Tyler Hall. All rights reserved. 7 | // 8 | 9 | #import "FMDB.h" 10 | -------------------------------------------------------------------------------- /photoz.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /photoz.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Photoz is a small command line tool that is specifically designed to merge multiple Google Photos backups (exported via Google Takeout) into a shared folder, and then organize and de-duplicate your photos/videos. 2 | 3 | Please see my blog post for more info: 4 | 5 | [https://tyler.io/merge-deduplicate-google-photos/](https://tyler.io/merge-deduplicate-google-photos/) 6 | -------------------------------------------------------------------------------- /photoz.xcodeproj/xcuserdata/thall.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | photoz.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tyler Hall 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /photoz/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // photoz 4 | // 5 | // Created by Tyler Hall on 7/29/20. 6 | // Copyright © 2020 Tyler Hall. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension URL { 12 | var isAlbum: Bool { 13 | var isDir: ObjCBool = false 14 | let exists = FileManager.default.fileExists(atPath: self.path, isDirectory: &isDir) 15 | guard exists && isDir.boolValue else { return false } 16 | 17 | let lastComponent = self.lastPathComponent 18 | return lastComponent.range(of: #"^[0-9]{4}-[0-9]{2}\s+.*$"#, options: .regularExpression) != nil 19 | } 20 | 21 | var isYearMonthFolder: Bool { 22 | var isDir: ObjCBool = false 23 | let exists = FileManager.default.fileExists(atPath: self.path, isDirectory: &isDir) 24 | guard exists && isDir.boolValue else { return false } 25 | 26 | let lastComponent = self.lastPathComponent 27 | return lastComponent.range(of: #"^[0-9]{4}-[0-9]{2}$"#, options: .regularExpression) != nil 28 | } 29 | 30 | var isYearMonthDayFolder: Bool { 31 | var isDir: ObjCBool = false 32 | let exists = FileManager.default.fileExists(atPath: self.path, isDirectory: &isDir) 33 | guard exists && isDir.boolValue else { return false } 34 | 35 | let lastComponent = self.lastPathComponent 36 | return lastComponent.range(of: #"^[0-9]{4}-[0-9]{2}-[0-9]{2}"#, options: .regularExpression) != nil 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | Packages/ 39 | Package.pins 40 | Package.resolved 41 | .build/ 42 | .swiftpm 43 | 44 | # CocoaPods 45 | # 46 | # We recommend against adding the Pods directory to your .gitignore. However 47 | # you should judge for yourself, the pros and cons are mentioned at: 48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 49 | # 50 | Pods/ 51 | 52 | # Carthage 53 | # 54 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 55 | Carthage/Checkouts 56 | 57 | Carthage/Build 58 | 59 | # fastlane 60 | # 61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 62 | # screenshots whenever they are needed. 63 | # For more information about the recommended setup visit: 64 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 65 | 66 | fastlane/report.xml 67 | fastlane/Preview.html 68 | fastlane/screenshots/**/*.png 69 | fastlane/test_output 70 | 71 | .DS_Store 72 | -------------------------------------------------------------------------------- /photoz/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // photoz 4 | // 5 | // Created by Tyler Hall on 8/22/20. 6 | // Copyright © 2020 Tyler Hall. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import FMDB 11 | import Checksum 12 | 13 | let dryRun = false 14 | let noisy = true 15 | 16 | try? FileManager.default.removeItem(atPath: "photos.sqlite") 17 | let queue = FMDatabaseQueue(path: "photos.sqlite") 18 | 19 | let operationQueue = OperationQueue() 20 | operationQueue.maxConcurrentOperationCount = (ProcessInfo().processorCount * 2) 21 | 22 | var importURL: URL! 23 | var libraryURL: URL! 24 | 25 | enum DirType: Int { 26 | case Album 27 | case YearMonth 28 | case YearMonthDay 29 | case Unknown 30 | } 31 | 32 | if CommandLine.arguments.count == 4 { 33 | guard CommandLine.arguments[1] == "import" else { 34 | print("Invalid arguments") 35 | exit(EXIT_FAILURE) 36 | } 37 | 38 | let importDir = CommandLine.arguments[2] 39 | importURL = URL(fileURLWithPath: importDir) 40 | 41 | let libraryDir = CommandLine.arguments[3] 42 | libraryURL = URL(fileURLWithPath: libraryDir) 43 | 44 | mergeIntoLibrary() 45 | 46 | exit(EXIT_SUCCESS) 47 | } 48 | 49 | if CommandLine.arguments.count == 3 { 50 | guard CommandLine.arguments[1] == "organize" else { 51 | print("Invalid arguments") 52 | exit(EXIT_FAILURE) 53 | } 54 | 55 | let libraryDir = CommandLine.arguments[2] 56 | libraryURL = URL(fileURLWithPath: libraryDir) 57 | 58 | queue?.inDatabase({ (db) in 59 | db.executeStatements("CREATE TABLE IF NOT EXISTS photo_hashes (path TEXT, hash TEXT, dir_type INTEGER);") 60 | }) 61 | 62 | generateAllHashes() 63 | organizeLibrary() 64 | cleanupLibrary() 65 | 66 | exit(EXIT_SUCCESS) 67 | } 68 | 69 | print("Usage: photoz import /path/to/photos /path/to/library") 70 | print("Usage: photoz organize /path/to/library") 71 | exit(EXIT_FAILURE) 72 | -------------------------------------------------------------------------------- /photoz/Methods.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Methods.swift 3 | // photoz 4 | // 5 | // Created by Tyler Hall on 8/22/20. 6 | // Copyright © 2020 Tyler Hall. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Checksum 11 | 12 | func generateAllHashes() { 13 | // I prefer to crash if this fails for some reason 14 | let directories = try! FileManager.default.contentsOfDirectory(at: libraryURL, includingPropertiesForKeys: nil, options: [FileManager.DirectoryEnumerationOptions.skipsHiddenFiles]) 15 | for dirURL in directories { 16 | print("# Scanning Directory: \(dirURL.path)") 17 | 18 | var dirType: DirType 19 | if dirURL.isAlbum { 20 | dirType = .Album 21 | } else if dirURL.isYearMonthFolder { 22 | dirType = .YearMonth 23 | } else if dirURL.isYearMonthDayFolder { 24 | dirType = .YearMonthDay 25 | } else { 26 | continue // I don't want to touch directories that aren't in a format I'm expecting. 27 | } 28 | 29 | guard let files = try? FileManager.default.contentsOfDirectory(at: dirURL, includingPropertiesForKeys: nil, options: [FileManager.DirectoryEnumerationOptions.skipsHiddenFiles]) else { continue } 30 | 31 | for fileURL in files { 32 | var isDir: ObjCBool = false 33 | let exists = FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDir) 34 | if !exists || isDir.boolValue { 35 | continue 36 | } 37 | 38 | if(fileURL.pathExtension.lowercased() == "json") { 39 | try? FileManager.default.removeItem(at: fileURL) 40 | continue 41 | } 42 | 43 | let op = BlockOperation { 44 | print("## Calculating Hash \(operationQueue.operationCount): \(fileURL.path)") 45 | if let md5 = fileURL.checksum(algorithm: .md5) { 46 | let lib = libraryURL.path 47 | let file = fileURL.path 48 | let relative = file.replacingOccurrences(of: lib, with: "") 49 | 50 | queue?.inDatabase({ (db) in 51 | do { 52 | try db.executeUpdate("INSERT INTO photo_hashes (path, hash, dir_type) VALUES (?, ?, ?)", values: [relative, md5, dirType.rawValue]) 53 | } catch { 54 | fatalError("Could not insert hash for \(fileURL.path)") 55 | } 56 | }) 57 | } 58 | } 59 | operationQueue.addOperation(op) 60 | } 61 | } 62 | 63 | operationQueue.waitUntilAllOperationsAreFinished() 64 | } 65 | 66 | func organizeLibrary() { 67 | queue?.inDatabase({ (db) in 68 | guard let results = try? db.executeQuery("SELECT * FROM photo_hashes WHERE dir_type = ?", values: [DirType.YearMonthDay.rawValue]) else { fatalError() } 69 | 70 | while results.next() { 71 | guard let path = results.string(forColumn: "path") else { fatalError() } 72 | guard let hash = results.string(forColumn: "hash") else { fatalError() } 73 | 74 | guard let countResults = try? db.executeQuery("SELECT COUNT(*) FROM photo_hashes WHERE hash = ? AND dir_type != ?", values: [hash, DirType.YearMonthDay.rawValue]) else { fatalError() } 75 | if countResults.next() { 76 | let count = countResults.int(forColumnIndex: 0) 77 | if count == 0 { 78 | print("### Move \(path)") 79 | let originalFileURL = libraryURL.appendingPathComponent(path) 80 | moveFileIntoAppropriateYearMonthAlbum(originalFileURL) 81 | } else { 82 | print("### Duplicate \(path)") 83 | } 84 | } 85 | countResults.close() 86 | } 87 | }) 88 | } 89 | 90 | func moveFileIntoAppropriateYearMonthAlbum(_ fileURL: URL) { 91 | let folderURL = fileURL.deletingLastPathComponent() 92 | guard folderURL.isYearMonthDayFolder else { return } 93 | 94 | let filename = fileURL.lastPathComponent 95 | let yearMonth = String(folderURL.lastPathComponent.prefix(7)) // "YYYY-MM" 96 | let destDirURL = libraryURL.appendingPathComponent(yearMonth) 97 | 98 | var destURL = destDirURL.appendingPathComponent(filename) 99 | var numericSuffix = 0 100 | while(FileManager.default.fileExists(atPath: destURL.path)) { 101 | numericSuffix += 1 102 | 103 | if let dotIndex = filename.lastIndex(of: ".") { 104 | var potentialFilename = filename 105 | potentialFilename.insert(contentsOf: " (\(numericSuffix))", at: dotIndex) 106 | destURL = destDirURL.appendingPathComponent(potentialFilename) 107 | } else { 108 | let potentialFilename = filename + " (\(numericSuffix))" 109 | destURL = destDirURL.appendingPathComponent(potentialFilename) 110 | } 111 | } 112 | 113 | print("###### ==> \(destURL.path)") 114 | try? FileManager.default.createDirectory(at: destDirURL, withIntermediateDirectories: true, attributes: nil) 115 | try! FileManager.default.moveItem(at: fileURL, to: destURL) // I prefer to crash if this fails for some reason 116 | } 117 | 118 | func cleanupLibrary() { 119 | queue?.inDatabase({ (db) in 120 | guard let results = try? db.executeQuery("SELECT * FROM photo_hashes WHERE dir_type = ? OR dir_type = ?", values: [DirType.YearMonthDay.rawValue, DirType.YearMonth.rawValue]) else { fatalError() } 121 | 122 | while results.next() { 123 | guard let path = results.string(forColumn: "path") else { fatalError() } 124 | guard let hash = results.string(forColumn: "hash") else { fatalError() } 125 | 126 | guard let countResults = try? db.executeQuery("SELECT COUNT(*) FROM photo_hashes WHERE hash = ? AND dir_type = ?", values: [hash, DirType.Album.rawValue]) else { fatalError() } 127 | if countResults.next() { 128 | let count = countResults.int(forColumnIndex: 0) 129 | if count > 0 { 130 | let originalFileURL = libraryURL.appendingPathComponent(path) 131 | print("### Found date photo in album \(originalFileURL.path)") 132 | print("###### DELETING") 133 | try? FileManager.default.removeItem(at: originalFileURL) 134 | } 135 | } 136 | countResults.close() 137 | } 138 | }) 139 | 140 | // I prefer to crash if this fails for some reason 141 | let directories = try! FileManager.default.contentsOfDirectory(at: libraryURL, includingPropertiesForKeys: nil, options: [FileManager.DirectoryEnumerationOptions.skipsHiddenFiles]) 142 | for dirURL in directories { 143 | if dirURL.isYearMonthDayFolder { 144 | print("### DELETING year-month-day folder \(dirURL.path)") 145 | try? FileManager.default.removeItem(at: dirURL) 146 | } 147 | } 148 | } 149 | 150 | func mergeIntoLibrary() { 151 | let directories = try! FileManager.default.contentsOfDirectory(at: importURL, includingPropertiesForKeys: nil, options: [FileManager.DirectoryEnumerationOptions.skipsHiddenFiles]) 152 | for dirURL in directories { 153 | print("# Scanning Import Directory: \(dirURL.path)") 154 | 155 | let dirName = dirURL.lastPathComponent 156 | let dirLibURL = libraryURL.appendingPathComponent(dirName) 157 | try? FileManager.default.createDirectory(at: dirLibURL, withIntermediateDirectories: true, attributes: nil) 158 | 159 | guard let files = try? FileManager.default.contentsOfDirectory(at: dirURL, includingPropertiesForKeys: nil, options: [FileManager.DirectoryEnumerationOptions.skipsHiddenFiles]) else { continue } 160 | 161 | for fileURL in files { 162 | var isDir: ObjCBool = false 163 | let exists = FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDir) 164 | if !exists || isDir.boolValue { 165 | continue 166 | } 167 | 168 | if(fileURL.pathExtension.lowercased() == "json") { 169 | try? FileManager.default.removeItem(at: fileURL) 170 | continue 171 | } 172 | 173 | let filename = fileURL.lastPathComponent 174 | var destURL = dirLibURL.appendingPathComponent(filename) 175 | var numericSuffix = 0 176 | while(FileManager.default.fileExists(atPath: destURL.path)) { 177 | numericSuffix += 1 178 | 179 | if let dotIndex = filename.lastIndex(of: ".") { 180 | var potentialFilename = filename 181 | potentialFilename.insert(contentsOf: " (\(numericSuffix))", at: dotIndex) 182 | destURL = dirLibURL.appendingPathComponent(potentialFilename) 183 | } else { 184 | let potentialFilename = filename + " (\(numericSuffix))" 185 | destURL = dirLibURL.appendingPathComponent(potentialFilename) 186 | } 187 | } 188 | 189 | try! FileManager.default.moveItem(at: fileURL, to: destURL) // I prefer to crash if this fails for some reason 190 | } 191 | } 192 | 193 | operationQueue.waitUntilAllOperationsAreFinished() 194 | } 195 | -------------------------------------------------------------------------------- /photoz.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C62B3AA124F216B300C1748A /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C62B3AA024F216B300C1748A /* main.swift */; }; 11 | C62B3AA924F216D600C1748A /* Checksum in Frameworks */ = {isa = PBXBuildFile; productRef = C62B3AA824F216D600C1748A /* Checksum */; }; 12 | C62B3AAC24F216E500C1748A /* FMDB in Frameworks */ = {isa = PBXBuildFile; productRef = C62B3AAB24F216E500C1748A /* FMDB */; }; 13 | C62B3AAF24F2171D00C1748A /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C62B3AAE24F2171D00C1748A /* Extensions.swift */; }; 14 | C62B3AB424F217B200C1748A /* Methods.swift in Sources */ = {isa = PBXBuildFile; fileRef = C62B3AB324F217B200C1748A /* Methods.swift */; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXCopyFilesBuildPhase section */ 18 | C62B3A9B24F216B300C1748A /* CopyFiles */ = { 19 | isa = PBXCopyFilesBuildPhase; 20 | buildActionMask = 2147483647; 21 | dstPath = /usr/share/man/man1/; 22 | dstSubfolderSpec = 0; 23 | files = ( 24 | ); 25 | runOnlyForDeploymentPostprocessing = 1; 26 | }; 27 | /* End PBXCopyFilesBuildPhase section */ 28 | 29 | /* Begin PBXFileReference section */ 30 | C62B3A9D24F216B300C1748A /* photoz */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = photoz; sourceTree = BUILT_PRODUCTS_DIR; }; 31 | C62B3AA024F216B300C1748A /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 32 | C62B3AAD24F216F700C1748A /* Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Bridging-Header.h"; sourceTree = ""; }; 33 | C62B3AAE24F2171D00C1748A /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 34 | C62B3AB324F217B200C1748A /* Methods.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Methods.swift; sourceTree = ""; }; 35 | /* End PBXFileReference section */ 36 | 37 | /* Begin PBXFrameworksBuildPhase section */ 38 | C62B3A9A24F216B300C1748A /* Frameworks */ = { 39 | isa = PBXFrameworksBuildPhase; 40 | buildActionMask = 2147483647; 41 | files = ( 42 | C62B3AA924F216D600C1748A /* Checksum in Frameworks */, 43 | C62B3AAC24F216E500C1748A /* FMDB in Frameworks */, 44 | ); 45 | runOnlyForDeploymentPostprocessing = 0; 46 | }; 47 | /* End PBXFrameworksBuildPhase section */ 48 | 49 | /* Begin PBXGroup section */ 50 | C62B3A9424F216B200C1748A = { 51 | isa = PBXGroup; 52 | children = ( 53 | C62B3A9F24F216B300C1748A /* photoz */, 54 | C62B3A9E24F216B300C1748A /* Products */, 55 | ); 56 | sourceTree = ""; 57 | }; 58 | C62B3A9E24F216B300C1748A /* Products */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | C62B3A9D24F216B300C1748A /* photoz */, 62 | ); 63 | name = Products; 64 | sourceTree = ""; 65 | }; 66 | C62B3A9F24F216B300C1748A /* photoz */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | C62B3AA024F216B300C1748A /* main.swift */, 70 | C62B3AB324F217B200C1748A /* Methods.swift */, 71 | C62B3AAE24F2171D00C1748A /* Extensions.swift */, 72 | C62B3AAD24F216F700C1748A /* Bridging-Header.h */, 73 | ); 74 | path = photoz; 75 | sourceTree = ""; 76 | }; 77 | /* End PBXGroup section */ 78 | 79 | /* Begin PBXNativeTarget section */ 80 | C62B3A9C24F216B300C1748A /* photoz */ = { 81 | isa = PBXNativeTarget; 82 | buildConfigurationList = C62B3AA424F216B300C1748A /* Build configuration list for PBXNativeTarget "photoz" */; 83 | buildPhases = ( 84 | C62B3A9924F216B300C1748A /* Sources */, 85 | C62B3A9A24F216B300C1748A /* Frameworks */, 86 | C62B3A9B24F216B300C1748A /* CopyFiles */, 87 | ); 88 | buildRules = ( 89 | ); 90 | dependencies = ( 91 | ); 92 | name = photoz; 93 | packageProductDependencies = ( 94 | C62B3AA824F216D600C1748A /* Checksum */, 95 | C62B3AAB24F216E500C1748A /* FMDB */, 96 | ); 97 | productName = photoz; 98 | productReference = C62B3A9D24F216B300C1748A /* photoz */; 99 | productType = "com.apple.product-type.tool"; 100 | }; 101 | /* End PBXNativeTarget section */ 102 | 103 | /* Begin PBXProject section */ 104 | C62B3A9524F216B200C1748A /* Project object */ = { 105 | isa = PBXProject; 106 | attributes = { 107 | LastSwiftUpdateCheck = 1160; 108 | LastUpgradeCheck = 1160; 109 | ORGANIZATIONNAME = "Tyler Hall"; 110 | TargetAttributes = { 111 | C62B3A9C24F216B300C1748A = { 112 | CreatedOnToolsVersion = 11.6; 113 | }; 114 | }; 115 | }; 116 | buildConfigurationList = C62B3A9824F216B200C1748A /* Build configuration list for PBXProject "photoz" */; 117 | compatibilityVersion = "Xcode 9.3"; 118 | developmentRegion = en; 119 | hasScannedForEncodings = 0; 120 | knownRegions = ( 121 | en, 122 | Base, 123 | ); 124 | mainGroup = C62B3A9424F216B200C1748A; 125 | packageReferences = ( 126 | C62B3AA724F216D600C1748A /* XCRemoteSwiftPackageReference "Checksum" */, 127 | C62B3AAA24F216E500C1748A /* XCRemoteSwiftPackageReference "fmdb" */, 128 | ); 129 | productRefGroup = C62B3A9E24F216B300C1748A /* Products */; 130 | projectDirPath = ""; 131 | projectRoot = ""; 132 | targets = ( 133 | C62B3A9C24F216B300C1748A /* photoz */, 134 | ); 135 | }; 136 | /* End PBXProject section */ 137 | 138 | /* Begin PBXSourcesBuildPhase section */ 139 | C62B3A9924F216B300C1748A /* Sources */ = { 140 | isa = PBXSourcesBuildPhase; 141 | buildActionMask = 2147483647; 142 | files = ( 143 | C62B3AA124F216B300C1748A /* main.swift in Sources */, 144 | C62B3AB424F217B200C1748A /* Methods.swift in Sources */, 145 | C62B3AAF24F2171D00C1748A /* Extensions.swift in Sources */, 146 | ); 147 | runOnlyForDeploymentPostprocessing = 0; 148 | }; 149 | /* End PBXSourcesBuildPhase section */ 150 | 151 | /* Begin XCBuildConfiguration section */ 152 | C62B3AA224F216B300C1748A /* Debug */ = { 153 | isa = XCBuildConfiguration; 154 | buildSettings = { 155 | ALWAYS_SEARCH_USER_PATHS = NO; 156 | CLANG_ANALYZER_NONNULL = YES; 157 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 158 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 159 | CLANG_CXX_LIBRARY = "libc++"; 160 | CLANG_ENABLE_MODULES = YES; 161 | CLANG_ENABLE_OBJC_ARC = YES; 162 | CLANG_ENABLE_OBJC_WEAK = YES; 163 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 164 | CLANG_WARN_BOOL_CONVERSION = YES; 165 | CLANG_WARN_COMMA = YES; 166 | CLANG_WARN_CONSTANT_CONVERSION = YES; 167 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 168 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 169 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 170 | CLANG_WARN_EMPTY_BODY = YES; 171 | CLANG_WARN_ENUM_CONVERSION = YES; 172 | CLANG_WARN_INFINITE_RECURSION = YES; 173 | CLANG_WARN_INT_CONVERSION = YES; 174 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 175 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 176 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 177 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 178 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 179 | CLANG_WARN_STRICT_PROTOTYPES = YES; 180 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 181 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 182 | CLANG_WARN_UNREACHABLE_CODE = YES; 183 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 184 | COPY_PHASE_STRIP = NO; 185 | DEBUG_INFORMATION_FORMAT = dwarf; 186 | ENABLE_STRICT_OBJC_MSGSEND = YES; 187 | ENABLE_TESTABILITY = YES; 188 | GCC_C_LANGUAGE_STANDARD = gnu11; 189 | GCC_DYNAMIC_NO_PIC = NO; 190 | GCC_NO_COMMON_BLOCKS = YES; 191 | GCC_OPTIMIZATION_LEVEL = 0; 192 | GCC_PREPROCESSOR_DEFINITIONS = ( 193 | "DEBUG=1", 194 | "$(inherited)", 195 | ); 196 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 197 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 198 | GCC_WARN_UNDECLARED_SELECTOR = YES; 199 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 200 | GCC_WARN_UNUSED_FUNCTION = YES; 201 | GCC_WARN_UNUSED_VARIABLE = YES; 202 | MACOSX_DEPLOYMENT_TARGET = 10.15; 203 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 204 | MTL_FAST_MATH = YES; 205 | ONLY_ACTIVE_ARCH = YES; 206 | SDKROOT = macosx; 207 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 208 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 209 | }; 210 | name = Debug; 211 | }; 212 | C62B3AA324F216B300C1748A /* Release */ = { 213 | isa = XCBuildConfiguration; 214 | buildSettings = { 215 | ALWAYS_SEARCH_USER_PATHS = NO; 216 | CLANG_ANALYZER_NONNULL = YES; 217 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 218 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 219 | CLANG_CXX_LIBRARY = "libc++"; 220 | CLANG_ENABLE_MODULES = YES; 221 | CLANG_ENABLE_OBJC_ARC = YES; 222 | CLANG_ENABLE_OBJC_WEAK = YES; 223 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 224 | CLANG_WARN_BOOL_CONVERSION = YES; 225 | CLANG_WARN_COMMA = YES; 226 | CLANG_WARN_CONSTANT_CONVERSION = YES; 227 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 228 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 229 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 230 | CLANG_WARN_EMPTY_BODY = YES; 231 | CLANG_WARN_ENUM_CONVERSION = YES; 232 | CLANG_WARN_INFINITE_RECURSION = YES; 233 | CLANG_WARN_INT_CONVERSION = YES; 234 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 235 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 236 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 237 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 238 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 239 | CLANG_WARN_STRICT_PROTOTYPES = YES; 240 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 241 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 242 | CLANG_WARN_UNREACHABLE_CODE = YES; 243 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 244 | COPY_PHASE_STRIP = NO; 245 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 246 | ENABLE_NS_ASSERTIONS = NO; 247 | ENABLE_STRICT_OBJC_MSGSEND = YES; 248 | GCC_C_LANGUAGE_STANDARD = gnu11; 249 | GCC_NO_COMMON_BLOCKS = YES; 250 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 251 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 252 | GCC_WARN_UNDECLARED_SELECTOR = YES; 253 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 254 | GCC_WARN_UNUSED_FUNCTION = YES; 255 | GCC_WARN_UNUSED_VARIABLE = YES; 256 | MACOSX_DEPLOYMENT_TARGET = 10.15; 257 | MTL_ENABLE_DEBUG_INFO = NO; 258 | MTL_FAST_MATH = YES; 259 | SDKROOT = macosx; 260 | SWIFT_COMPILATION_MODE = wholemodule; 261 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 262 | }; 263 | name = Release; 264 | }; 265 | C62B3AA524F216B300C1748A /* Debug */ = { 266 | isa = XCBuildConfiguration; 267 | buildSettings = { 268 | CODE_SIGN_STYLE = Automatic; 269 | DEVELOPMENT_TEAM = 3A6K89K388; 270 | ENABLE_HARDENED_RUNTIME = YES; 271 | PRODUCT_NAME = "$(TARGET_NAME)"; 272 | SWIFT_OBJC_BRIDGING_HEADER = "photoz/Bridging-Header.h"; 273 | SWIFT_VERSION = 5.0; 274 | }; 275 | name = Debug; 276 | }; 277 | C62B3AA624F216B300C1748A /* Release */ = { 278 | isa = XCBuildConfiguration; 279 | buildSettings = { 280 | CODE_SIGN_STYLE = Automatic; 281 | DEVELOPMENT_TEAM = 3A6K89K388; 282 | ENABLE_HARDENED_RUNTIME = YES; 283 | PRODUCT_NAME = "$(TARGET_NAME)"; 284 | SWIFT_OBJC_BRIDGING_HEADER = "photoz/Bridging-Header.h"; 285 | SWIFT_VERSION = 5.0; 286 | }; 287 | name = Release; 288 | }; 289 | /* End XCBuildConfiguration section */ 290 | 291 | /* Begin XCConfigurationList section */ 292 | C62B3A9824F216B200C1748A /* Build configuration list for PBXProject "photoz" */ = { 293 | isa = XCConfigurationList; 294 | buildConfigurations = ( 295 | C62B3AA224F216B300C1748A /* Debug */, 296 | C62B3AA324F216B300C1748A /* Release */, 297 | ); 298 | defaultConfigurationIsVisible = 0; 299 | defaultConfigurationName = Release; 300 | }; 301 | C62B3AA424F216B300C1748A /* Build configuration list for PBXNativeTarget "photoz" */ = { 302 | isa = XCConfigurationList; 303 | buildConfigurations = ( 304 | C62B3AA524F216B300C1748A /* Debug */, 305 | C62B3AA624F216B300C1748A /* Release */, 306 | ); 307 | defaultConfigurationIsVisible = 0; 308 | defaultConfigurationName = Release; 309 | }; 310 | /* End XCConfigurationList section */ 311 | 312 | /* Begin XCRemoteSwiftPackageReference section */ 313 | C62B3AA724F216D600C1748A /* XCRemoteSwiftPackageReference "Checksum" */ = { 314 | isa = XCRemoteSwiftPackageReference; 315 | repositoryURL = "https://github.com/rnine/Checksum"; 316 | requirement = { 317 | kind = upToNextMajorVersion; 318 | minimumVersion = 1.0.2; 319 | }; 320 | }; 321 | C62B3AAA24F216E500C1748A /* XCRemoteSwiftPackageReference "fmdb" */ = { 322 | isa = XCRemoteSwiftPackageReference; 323 | repositoryURL = "https://github.com/ccgus/fmdb"; 324 | requirement = { 325 | kind = upToNextMajorVersion; 326 | minimumVersion = 2.7.7; 327 | }; 328 | }; 329 | /* End XCRemoteSwiftPackageReference section */ 330 | 331 | /* Begin XCSwiftPackageProductDependency section */ 332 | C62B3AA824F216D600C1748A /* Checksum */ = { 333 | isa = XCSwiftPackageProductDependency; 334 | package = C62B3AA724F216D600C1748A /* XCRemoteSwiftPackageReference "Checksum" */; 335 | productName = Checksum; 336 | }; 337 | C62B3AAB24F216E500C1748A /* FMDB */ = { 338 | isa = XCSwiftPackageProductDependency; 339 | package = C62B3AAA24F216E500C1748A /* XCRemoteSwiftPackageReference "fmdb" */; 340 | productName = FMDB; 341 | }; 342 | /* End XCSwiftPackageProductDependency section */ 343 | }; 344 | rootObject = C62B3A9524F216B200C1748A /* Project object */; 345 | } 346 | --------------------------------------------------------------------------------