├── 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 |
--------------------------------------------------------------------------------