Add photo management scripts
parent
a8b69fec4c
commit
d28a647895
@ -0,0 +1,173 @@
|
|||||||
|
-- Fork of the autogroup plugin but only comparing pictures with the same timestamp
|
||||||
|
local dt = require "darktable"
|
||||||
|
local du = require "lib/dtutils"
|
||||||
|
local df = require "lib/dtutils.file"
|
||||||
|
|
||||||
|
du.check_min_api_version("7.0.0", "AutoGroupInstant")
|
||||||
|
|
||||||
|
local MOD = 'autogroupinstant'
|
||||||
|
|
||||||
|
-- return data structure for script_manager
|
||||||
|
|
||||||
|
local script_data = {}
|
||||||
|
|
||||||
|
script_data.destroy = nil -- function to destory the script
|
||||||
|
script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil
|
||||||
|
script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again
|
||||||
|
script_data.show = nil -- only required for libs since the destroy_method only hides them
|
||||||
|
|
||||||
|
local gettext = dt.gettext
|
||||||
|
-- Tell gettext where to find the .mo file translating messages for a particular domain
|
||||||
|
gettext.bindtextdomain("AutoGroupInstant",dt.configuration.config_dir.."/lua/locale/")
|
||||||
|
|
||||||
|
local function _(msgid)
|
||||||
|
return gettext.dgettext("AutoGroupInstant", msgid)
|
||||||
|
end
|
||||||
|
|
||||||
|
local Ag = {}
|
||||||
|
Ag.module_installed = false
|
||||||
|
Ag.event_registered = false
|
||||||
|
|
||||||
|
local GUI = {
|
||||||
|
gap = {},
|
||||||
|
selected = {},
|
||||||
|
collection = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
local function InRange(test, low, high) --tests if test value is within range of low and high (inclusive)
|
||||||
|
if test >= low and test <= high then
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function CompTime(first, second) --compares the timestamps and returns true if first was taken before second
|
||||||
|
first_time = first.exif_datetime_taken
|
||||||
|
if string.match(first_time, '[0-9]') == nil then first_time = '9999:99:99 99:99:99' end
|
||||||
|
first_time = tonumber(string.gsub(first_time, '[^0-9]*',''))
|
||||||
|
second_time = second.exif_datetime_taken
|
||||||
|
if string.match(second_time, '[0-9]') == nil then second_time = '9999:99:99 99:99:99' end
|
||||||
|
second_time = tonumber(string.gsub(second_time, '[^0-9]*',''))
|
||||||
|
return first_time < second_time
|
||||||
|
end
|
||||||
|
|
||||||
|
local function SeperateTime(str) --seperates the timestamp into individual components for used with OS.time operations
|
||||||
|
local cleaned = string.gsub(str, '[^%d]',':')
|
||||||
|
cleaned = string.gsub(cleaned, '::*',':') --YYYY:MM:DD:hh:mm:ss
|
||||||
|
local year = string.sub(cleaned,1,4)
|
||||||
|
local month = string.sub(cleaned,6,7)
|
||||||
|
local day = string.sub(cleaned,9,10)
|
||||||
|
local hour = string.sub(cleaned,12,13)
|
||||||
|
local min = string.sub(cleaned,15,16)
|
||||||
|
local sec = string.sub(cleaned,18,19)
|
||||||
|
return {year = year, month = month, day = day, hour = hour, min = min, sec = sec}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function GetTimeDiff(curr_image, prev_image) --returns the time difference (in sec.) from current image and the previous image
|
||||||
|
local curr_time = SeperateTime(curr_image.exif_datetime_taken)
|
||||||
|
local prev_time = SeperateTime(prev_image.exif_datetime_taken)
|
||||||
|
return os.time(curr_time)-os.time(prev_time)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function main(on_collection)
|
||||||
|
local images = {}
|
||||||
|
if on_collection then
|
||||||
|
local col_images = dt.collection
|
||||||
|
for i,image in ipairs(col_images) do --copy images to a standard table, table.sort barfs on type dt_lua_singleton_image_collection
|
||||||
|
table.insert(images,i,image)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
images = dt.gui.selection()
|
||||||
|
end
|
||||||
|
if #images < 2 then
|
||||||
|
dt.print('please select at least 2 images')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
table.sort(images, function(first, second) return CompTime(first,second) end) --sort images by timestamp
|
||||||
|
|
||||||
|
for i, image in ipairs(images) do
|
||||||
|
if i == 1 then
|
||||||
|
prev_image = image
|
||||||
|
elseif string.match(image.exif_datetime_taken, '[%d]') ~= nil then --make sure current image has a timestamp
|
||||||
|
local curr_image = image
|
||||||
|
|
||||||
|
if GetTimeDiff(curr_image, prev_image) == 0 then
|
||||||
|
images[i]:group_with(images[i-1])
|
||||||
|
|
||||||
|
for _, member in ipairs(images[i]:get_group_members()) do
|
||||||
|
local ext = string.lower(df.get_filetype(member.filename))
|
||||||
|
if ext == "rw2" or ext == "arw" then
|
||||||
|
member:make_group_leader()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
prev_image = curr_image
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function install_module()
|
||||||
|
if not Ag.module_installed then
|
||||||
|
dt.print_log("installing module")
|
||||||
|
dt.register_lib(
|
||||||
|
MOD, -- Module name
|
||||||
|
_('Group identical Timestamps'), -- name
|
||||||
|
true, -- expandable
|
||||||
|
true, -- resetable
|
||||||
|
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 99}}, -- containers
|
||||||
|
dt.new_widget("box"){
|
||||||
|
orientation = "vertical",
|
||||||
|
GUI.selected,
|
||||||
|
GUI.collection
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Ag.module_installed = true
|
||||||
|
dt.print_log("module installed")
|
||||||
|
dt.print_log("styles module visibility is " .. tostring(dt.gui.libs["styles"].visible))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function destroy()
|
||||||
|
dt.gui.libs[MOD].visible = false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function restart()
|
||||||
|
dt.gui.libs[MOD].visible = true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- GUI --
|
||||||
|
GUI.selected = dt.new_widget("button"){
|
||||||
|
label = _('auto group: selected'),
|
||||||
|
tooltip =_('auto group selected images'),
|
||||||
|
clicked_callback = function() main(false) end
|
||||||
|
}
|
||||||
|
GUI.collection = dt.new_widget("button"){
|
||||||
|
label = _('auto group: collection'),
|
||||||
|
tooltip =_('auto group the entire collection'),
|
||||||
|
clicked_callback = function() main(true) end
|
||||||
|
}
|
||||||
|
|
||||||
|
if dt.gui.current_view().id == "lighttable" then
|
||||||
|
install_module()
|
||||||
|
else
|
||||||
|
if not Ag.event_registered then
|
||||||
|
dt.register_event(
|
||||||
|
"AutoGroupInstant", "view-changed",
|
||||||
|
function(event, old_view, new_view)
|
||||||
|
if new_view.name == "lighttable" and old_view.name == "darkroom" then
|
||||||
|
install_module()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
Ag.event_registered = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
script_data.destroy = destroy
|
||||||
|
script_data.destroy_method = "hide"
|
||||||
|
script_data.restart = restart
|
||||||
|
script_data.show = restart
|
||||||
|
|
||||||
|
return script_data
|
@ -0,0 +1,4 @@
|
|||||||
|
require "tools/script_manager"
|
||||||
|
require "AutoGroupInstant"
|
||||||
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
|||||||
|
def main [] {
|
||||||
|
}
|
||||||
|
|
||||||
|
export def `main import` [
|
||||||
|
src: string # source path
|
||||||
|
dst: string # destination path
|
||||||
|
--exif-only # only take the exif date
|
||||||
|
--link # replace the originals with symlinks
|
||||||
|
--tag-origin # tag with origin folder name
|
||||||
|
] {
|
||||||
|
print "Importing"
|
||||||
|
if ("moved.csv" | path exists) == false {
|
||||||
|
"source,destination\n" | save moved.csv
|
||||||
|
}
|
||||||
|
let entries = ( glob $"($src | str trim --right --char '/')/**/*"
|
||||||
|
| each { try { ls -l $in } catch {|e| print_line -e $"Failed to get metadata of ($e)"; []} }
|
||||||
|
| where { ($in | length) > 0 }
|
||||||
|
| each { first }
|
||||||
|
| where type == 'file'
|
||||||
|
| where size > 0B
|
||||||
|
)
|
||||||
|
let total_count = $entries | length
|
||||||
|
print $"Copying ($total_count) entries"
|
||||||
|
|
||||||
|
( $entries
|
||||||
|
| enumerate
|
||||||
|
| par-each {|entry|
|
||||||
|
let file = $entry.item
|
||||||
|
print_line $file.name
|
||||||
|
progress $entry.index $total_count
|
||||||
|
let meta = exiftool -j $file.name | from json | get 0
|
||||||
|
mut date = $file.created?
|
||||||
|
|
||||||
|
if $date == null {
|
||||||
|
$date = $file.modified?
|
||||||
|
}
|
||||||
|
|
||||||
|
let path_parts = $file.name | path parse
|
||||||
|
mut using_exif = false
|
||||||
|
|
||||||
|
if $meta.DateTimeOriginal? != null {
|
||||||
|
try {
|
||||||
|
$date = (parse_exif_timestamp $meta.DateTimeOriginal $meta.OffsetTimeOriginal?)
|
||||||
|
$using_exif = true
|
||||||
|
} catch {|e|
|
||||||
|
print_line -e $"Failed to parse datetime ($meta.DateTimeOriginal) ($e)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if $using_exif == false and $exif_only {
|
||||||
|
let folder_rel = ( try {
|
||||||
|
$path_parts.parent | path relative-to $src | into string
|
||||||
|
} catch {
|
||||||
|
$path_parts.parent | path parse | get stem
|
||||||
|
})
|
||||||
|
let folder = $dst | path join "unknown" | path join $folder_rel
|
||||||
|
mkdir $folder
|
||||||
|
|
||||||
|
let file_name = $folder | path join $"($path_parts.stem).($path_parts.extension)"
|
||||||
|
archive-cp $file.name $file_name --tag=$tag_origin
|
||||||
|
|
||||||
|
print_line $"Inaccurate date for file ($file.name)"
|
||||||
|
progress ($entry.index + 1) $total_count
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_name = $"($date | format date '%Y-%m-%d_%H%M%S')_($path_parts.stem).($path_parts.extension)"
|
||||||
|
print_line $"Filename: ($file_name)"
|
||||||
|
progress $entry.index $total_count
|
||||||
|
let folder_name = $dst | path join ($date | format date "%Y/%Y-%m-%B")
|
||||||
|
|
||||||
|
print $"Destination folder: ($folder_name)"
|
||||||
|
mkdir $folder_name
|
||||||
|
archive-cp $file.name ($folder_name | path join $file_name) --link=$link --tag=$tag_origin
|
||||||
|
|
||||||
|
print_line $"Copied ($meta.SourceFile)"
|
||||||
|
progress ($entry.index + 1) $total_count
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse_exif_timestamp [timestamp: string, offset?: any] {
|
||||||
|
let components = $timestamp | split row " "
|
||||||
|
let date = $components | first
|
||||||
|
let time = $components | last
|
||||||
|
|
||||||
|
$"($date | str replace -a ':' '.') ($time)($offset)" | into datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
def archive-cp [src: string, dst: string, --link, --tag] {
|
||||||
|
rsync -X -U -p -t -g -o -u --info=ALL $src $dst
|
||||||
|
$"($src),($dst)\n" | save -a moved.csv
|
||||||
|
|
||||||
|
if $tag {
|
||||||
|
xattr -w user.xdg.tags ($src | path parse | get parent | path parse | get stem) $dst
|
||||||
|
}
|
||||||
|
if $link {
|
||||||
|
rm $src
|
||||||
|
ln -s $dst $src
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def progress [current: number, total: number] {
|
||||||
|
let progress_perc = ($current / $total) * 100
|
||||||
|
print $"($current) / ($total) \(($progress_perc | math round -p 2)%\): (0..($progress_perc / 5 | math round) | each { '#' } | str join '')(ansi -e F)"
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_line [...msg: any, -e] {
|
||||||
|
print --stderr=$e $"(ansi -e 2K)( $msg | each { into string } | str join ' ' )"
|
||||||
|
}
|
Loading…
Reference in New Issue