-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathReorganizeByTime.lua
More file actions
337 lines (286 loc) · 13.3 KB
/
ReorganizeByTime.lua
File metadata and controls
337 lines (286 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
LrDialogs = import 'LrDialogs'
LrDate = import 'LrDate'
LrLogger = import 'LrLogger'
LrPathUtils = import 'LrPathUtils'
LrFileUtils = import 'LrFileUtils'
LrErrors = import 'LrErrors'
LrApplication = import "LrApplication"
LrProgressScope = import "LrProgressScope"
myLogger = LrLogger( 'libraryLogger' )
LrTasks = import 'LrTasks'
LrFunctionContext = import 'LrFunctionContext'
myLogger:enable( "print" ) -- or "logfile"
MyHWExportItem = {}
function table_print (tt, indent, done)
done = done or {}
indent = indent or 0
if type(tt) == "table" then
local sb = {}
for key, value in pairs (tt) do
table.insert(sb, string.rep (" ", indent)) -- indent it
if type (value) == "table" and not done [value] then
done [value] = true
table.insert(sb, "{\n");
table.insert(sb, table_print (value, indent + 2, done))
table.insert(sb, string.rep (" ", indent)) -- indent it
table.insert(sb, "}\n");
elseif "number" == type(key) then
table.insert(sb, string.format("\"%s\"\n", tostring(value)))
else
table.insert(sb, string.format(
"%s = \"%s\"\n", tostring (key), tostring(value)))
end
end
return table.concat(sb)
else
return tt .. "\n"
end
end
-- function groupMessages(array)
-- result = {};
-- for k, v in ipairs(array) do
-- if not result[v.sender] then
-- result[v.sender] = {};
-- end
-- table.insert(result[v.sender], v);
-- end
-- return result;
-- end
function Split(s, delimiter)
local result = {};
for match in (s..delimiter):gmatch("(.-)"..delimiter) do
table.insert(result, match);
end
return result;
end
function string.starts(String,Start)
return string.sub(String,1,string.len(Start)) == Start
end
function is_estimated_dt(dt)
if dt == nil then
return true
end
local year, month, day, hour, minute, second = LrDate.timestampToComponents(dt)
-- if minute and second are 0, assume it was manually inputted
return (minute == 0) and (second == 0)
end
function filter(predicate, orig_table)
local result = {}
for i, v in ipairs(orig_table) do
if predicate(v) then
table.insert(result, v)
end
end
return result
end
function flag_and_filter_if_any(predicate, photos, flag)
-- If predicate evaluates true for any of photos, flag the rest with a color
-- return the filtered set if any, the original if none
local filtered = filter(predicate, photos)
if #filtered > 0 then
for i, photo in ipairs(photos) do
if not predicate(photo) then
photo:setRawMetadata('colorNameForLabel', flag)
end
end
return filtered
end
return photos
end
function MyHWExportItem.reorganize_by_time()
catalog = LrApplication.activeCatalog()
catalog:withWriteAccessDo('reorganize_by_timestamp', function( context )
local all_folders = catalog:getFolders()
local names = {}
for k, v in pairs(all_folders) do
names[v:getName()] = v
end
local target_folder = names['Originals']
local base_path = target_folder:getPath()
local filenamePresets = LrApplication.filenamePresets()
local filename_preset = filenamePresets['ISO8601']
local scotty_preset = filenamePresets['ScottyG']
local photos = target_folder:getPhotos(true)
-- local photos = catalog:getTargetPhotos()
local total_photos = #photos
local badness = catalog:createCollectionSet( "Duplicates", nil, true )
local dupe_collection = catalog:createCollection(LrDate.timeToW3CDate(LrDate.currentTime()), badness)
local progress = LrProgressScope({title = "Detecting Duplicates"})
local index_progress = LrProgressScope({caption = "Creating index", title="creating index", parent=progress, parentEndRange=0.5})
local dupes = {}
progress:attachToFunctionContext(context)
progress:setCancelable( true )
index_progress:setCancelable( true )
local raw_metadata = catalog:batchGetRawMetadata( photos, {'gps', 'pickStatus', 'path', 'fileFormat', 'dimensions', 'isVirtualCopy', 'dateTimeOriginal', 'isVideo', 'dateTimeDigitized', 'dateTimeDigitizedISO8601'} )
local formatted_metadata = catalog:batchGetFormattedMetadata(photos, {'dateCreated', 'preservedFileName', 'gps', 'folderName'})
function has_gps(photo)
local gps = raw_metadata[photo]['gps']
if gps ~= nil then
local lat = gps['latitude']
return lat ~= nil
else
return false
end
end
function get_photo_size(photo)
local d = raw_metadata[photo]['dimensions']
return d['width'] * d['height']
end
function sort_mpix(p1, p2)
-- reverse order because of size priority - bigger should sort first
return get_photo_size(p2) < get_photo_size(p1)
end
local pi = 0
local uniques = 0
for pi, photo in ipairs(photos) do
if index_progress:isCanceled() then
break
end
local meta = raw_metadata[photo]
index_progress:setPortionComplete(pi-1, total_photos)
progress:setCaption("Deduping " .. tostring(pi) .. " of " .. tostring(total_photos))
-- current_path = photo:getRawMetadata('path')
-- leaf_name = LrPathUtils.leafName(current_path)
-- date_folder = LrPathUtils.leafName(LrPathUtils.parent(current_path))
-- valid_formats = {JPG=true, VIDEO=true}
if (meta['pickStatus'] >= 0) -- Skip rejected photos
and (not meta['isVirtualCopy']) -- and non-originals
-- and valid_formats[meta['fileFormat']
-- (meta['fileFormat'] == 'JPG') -- ignore RAW because faster shutter, more likely FP dupes by timestamp
-- or (meta['fileFormat'] == 'VIDEO'))
then
-- preset_filename = photo:getNameViaPreset( filename_preset, '', 0 )
-- current_path = photo:getRawMetadata('path')
-- current_path = meta['path']
-- leaf_name = LrPathUtils.leafName(current_path)
-- target_name = preset_filename .. photo:getRawMetadata('fileFormat')
-- target_name = preset_filename .. meta['fileFormat']
local fm = formatted_metadata[photo]
--is_estimated_dt(meta['dateTimeOriginal'])
-- use dateCreated because it has ms sometimes
-- fallback to digitized for movies etc (and use ISO because raw values are unstable?)
local dc = fm['dateCreated']
local dd = meta['dateTimeDigitizedISO8601']
local dto = meta['dateTimeOriginal']
local time_key = nil
local raw_time = meta['dateTimeDigitized']
if (
dc ~= nil and dc ~= ''
) then
time_key = dc
elseif (dd ~= nil and dd ~= '') then
time_key = dd
else
-- TS not available in metadata, but may still be in LR catalog?
-- Fall back to getting via filename preset
time_key = photo:getNameViaPreset( filename_preset, '', 0 )
-- photo:setRawMetadata('colorNameForLabel', 'purple')
end
-- skip DNG because cameras are faster (more FP) and also because they're more likely
-- imported correctly
if (meta['fileFormat'] ~= 'DNG') and (raw_time ~= nil and not is_estimated_dt(raw_time)) and time_key ~= nil then
-- to conform with preset which cannot use
-- time_key = time_key.gsub(':', '-')
local key = time_key .. tostring(meta['isVideo']) --.. fm['preservedFileName']
-- Insert into index by target filename
if not dupes[key] then
dupes[key] = {}
uniques = uniques + 1
end
table.insert(dupes[key], photo)
-- If the current filename doesn't match the target, it indicates likely sent or copied (?)
-- unclear why, the dates are initially wrong on import
-- Mark as red to make it easier deduplicating
-- if (leafName == nil or target_name == nil) then
-- -- bad metadata?
-- elseif string.starts(leafName, preset_filename) then
-- else
-- photo:setRawMetadata('colorNameForLabel', 'red')
-- end
end
end
-- target_parts = Split(target_name, '-')
-- current_relative = LrPathUtils.makeRelative(current_path, base_path)
-- target_relative = LrPathUtils.child(
-- target_parts[1],
-- LrPathUtils.child(
-- target_parts[1] .. '-' .. target_parts[2],
-- LrPathUtils.child(
-- target_parts[1] .. '-' .. target_parts[2] .. '-' .. target_parts[3],
-- target_name
-- )
-- )
-- )
-- if string.starts(current_relative, target_relative) then
-- else
-- -- dest = LrFileUtils.chooseUniqueFileName(LrPathUtils.addExtension(target_relative, LrPathUtils.extension(current_relative)))
-- -- dest_path = LrPathUtils.makeAbsolute(dest, base_path)
-- moved = moved + 1
-- -- break
-- photo:setRawMetadata('colorNameForLabel', 'red')
-- -- Note moving does not ensure photo is recognized by LR catalog! Seems not possible in API
-- -- break
-- -- LrFileUtils.createAllDirectories( dest_path )
-- -- LrFileUtils.move( current_path, dest_path )
-- -- break
-- end
end
index_progress:done()
-- LrDialogs.showBezel("Processed " .. tostring(pi) .. ' images')
local collection_progress = LrProgressScope({caption = "Creating dupes collection", parent=progress})
local i = 0
for key, dupe_photos in pairs(dupes) do
if progress:isCanceled() then
break
end
progress:setCaption('Processing ' .. tostring(i) .. " of " .. tostring(uniques) .. ' uniques')
collection_progress:setPortionComplete(i, uniques)
local ndupes = #dupe_photos
if ndupes > 1 then
if ndupes < 10 then
table.sort(dupe_photos, sort_mpix)
-- huge # of dupes likely due to incorrect DT
local max_size = get_photo_size(dupe_photos[1])
function not_cropped (photo)
return get_photo_size(photo) == max_size
end
local filtered = flag_and_filter_if_any(not_cropped, dupe_photos, 'yellow')
-- Look for any w/ GPS coords
filtered = flag_and_filter_if_any(has_gps, filtered, 'blue')
-- take HEIC over jpg
function is_heic(photo)
return raw_metadata[photo]['fileFormat'] == 'HEIC'
end
filtered = flag_and_filter_if_any(is_heic, filtered, 'purple')
-- See which are in the wrong folder
function correct_date_folder(photo)
local folderName = formatted_metadata[photo]['folderName']
-- First 10 chars are date: YYYY-mm-dd
return string.starts(folderName, string.sub(key, 1, 10))
end
filtered = flag_and_filter_if_any(correct_date_folder, filtered, 'red')
-- By default, get rid of any w/o exactly correct filename
function correct_file_name(photo)
local fn = LrPathUtils.leafName(raw_metadata[photo]['path'])
local target_name = photo:getNameViaPreset( scotty_preset, '', 0 )
return LrPathUtils.removeExtension(fn) == target_name
end
filtered = flag_and_filter_if_any(correct_file_name, filtered, 'green')
-- Arbitrarily unset color for the first remaining, and flag the rest
for fi, p in ipairs(filtered) do
if fi == 1 then
p:setRawMetadata('colorNameForLabel', 'none')
else
p:setRawMetadata('colorNameForLabel', 'green')
end
end
-- Add all dupes to collection
dupe_collection:addPhotos(dupe_photos)
-- break
end
end
i = i + 1
end
end )
end
LrTasks.startAsyncTask(MyHWExportItem.reorganize_by_time)