Last active
October 15, 2021 14:33
-
-
Save mproffitt/d720a1a513cd3155316783a023f0edf2 to your computer and use it in GitHub Desktop.
dtcp Darktable image management command
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# dtcp.bash | |
# | |
# darktable copy - workflow and image management script | |
# | |
# @author Martin Proffitt | |
# | |
# Script flags | |
# | |
# --install Install the script to /usr/local/bin, setup dependencies and systemd services | |
# --input Monitor the input directory for new files | |
# --output Monitor the output directory for fresh exports | |
# --resize Resize a given image file | |
# --resize-all Resize all image files under a directory (recursive) | |
# | |
# Dependencies | |
# | |
# - imagemagick | |
# - inotify-tools | |
# - exiftools | |
# | |
# Image location definitions | |
# | |
# For this particular workflow, we rely on a directory structure under | |
# the Pictures directory on linux. | |
# | |
# New images should be loaded into the "input" folder which is monitored by | |
# inotifywait. From here, all images will be transferred to the workbench | |
# folder, organised by image type, then date. This gives flexibility in | |
# finding individual daily filom-rolls within darktable. | |
# | |
# Exports from Darktable should be placed into the finals/darktable folder | |
# as this too is monitored by inotifywait for resizing and moving using imagemagick. | |
# | |
# Resized images will be placed into Pictures/final/resized organised by date then type | |
# as this makes it simpler to find all images of a particular type exported for a given | |
# days film roll. | |
PICTURES_DIR="${HOME}/Pictures" | |
INPUT_DIR="${PICTURES_DIR}/input" | |
WORKBENCH="${PICTURES_DIR}/workbench" | |
FINALS="${PICTURES_DIR}/final" | |
DARKTABLE="${FINALS}/darktable" | |
RESIZED="${FINALS}/resized" | |
# Create the necessary directories | |
DIRS=( | |
"${PICTURES_DIR}" | |
"${INPUT_DIR}" | |
"${WORKBENCH}" | |
"${FINALS}" | |
"${DARKTABLE}" | |
"${RESIZED}" | |
) | |
for directory in ${DIRS[@]}; do | |
[ ! -d ${directory} ] && mkdir -p ${directory} | |
done | |
# List of sizes to be exported by default | |
# Add or remove any sizes as necessary. | |
# Additional resizing needs to be added within the resize function | |
# as this list is for basic resizing only | |
SIZES=( | |
320 | |
640 | |
1024 | |
1920 | |
"original" | |
) | |
## | |
# Move files from the input folder to the workbench sorted by type > date | |
# | |
# @param filename String | |
# | |
function input() | |
{ | |
cd ${PICTURES_DIR} | |
local filename="$(basename $1)" | |
local ext="${filename##*.}" | |
# I tend to locate cr2 and dng together as they are both raw extension types | |
[ "${ext}" == 'dng' ] && ext=raw | |
[ "${ext}" == 'cr2' ] && ext=raw | |
if [ "$(cut -d_ -f1 <<<${filename})" == 'IMG' ] ; then | |
mv "input/${filename}" "input/$(cut -d_ -f2- <<<${filename})" | |
file="$(cut -d_ -f2- <<<${filename})" | |
fi | |
# Take the date from the filename instead of using todays date | |
# that allows us to find files by day of shoot | |
date=$(date -d $(cut -d_ -f1 <<< ${filename}) +%Y-%m-%d) | |
# Create the daily filmroll folder if it doesn't exist | |
[ ! -d "${WORKBENCH}/${ext}/${date}" ] && mkdir -p "${WORKBENCH}/${ext}/${date}" | |
# Don't overwrite images - get rid of duplicates instead | |
if [ -f "${WORKBENCH}/${ext}/${date}/${filename}" ] ; then | |
rm "${INPUT_DIR}/${filename}" | |
else | |
mv "${INPUT_DIR}/${filename}" "${WORKBENCH}/${ext}/${date}/${filename}" | |
fi | |
} | |
## | |
# Resize function | |
# | |
# @param filename String | |
# | |
# Resizes the given image based on the predefined list of sizes given above. | |
# | |
function resize() | |
{ | |
local filename="$(basename $1)" | |
local ext="${filename##*.}" | |
date=$(date -d $(cut -d_ -f1 <<< ${filename}) +%Y-%m-%d) | |
local path="${RESIZED}/${date}/${ext}" | |
# Loop over all sizes defined at the global level | |
for size in ${SIZES[@]}; do | |
[ ! -d "${path}/${size}" ] && mkdir -p "${path}/${size}" | |
# create the original directory (line above) but don't copy the file there | |
# yet as we may still want to create resizes against it | |
if [ "${size}" != 'original' ] ; then | |
mogrify -path ${path}/${size} -resize ${size} ${DARKTABLE}/${filename} | |
fi | |
done | |
# Any additional sizing should be done here before the move command. | |
# finally move the original image to the days "original" folder | |
# note, this is the original export, not the raw. | |
mv ${DARKTABLE}/${filename} ${path}/original/${filename} | |
} | |
## | |
# Install the script and dependencies | |
# | |
# This script will install to /usr/local/bin | |
# and set up service files in /usr/lib/systemd/user | |
# | |
function install() | |
{ | |
local scriptpath=$(dirname $1) | |
local script=$(basename $1) | |
# We require root priviledges to install this script | |
sudo mv ${scriptpath}/${script} /usr/local/bin/${script} | |
# debian and redhat dependency install | |
if [ -f /etc/lsb-release ] ; then | |
sudo apt install -y imagemagick inotify-tools libimage-exiftool-perl | |
elif [ -f /etc/redhat-release ] ; then | |
sudo dnf install -y imagemagick inotify-tools perl-Image-ExifTool.noarch | |
fi | |
for action in input output; do | |
service=dtcp-${action}-${USER}.service | |
cat <<EOF | sudo tee /usr/lib/systemd/user/${service} | |
[Service] | |
ExecStart=/usr/local/bin/${script} --${action} | |
Restart=always | |
RestartSec=1 | |
StandardOutput=syslog | |
StandardError=syslog | |
SyslogIdentifier=dtcp-${action}-${USER} | |
UMask=0077 | |
[Install] | |
WantedBy=multi-user.target | |
EOF | |
if systemctl --user is-enabled --quiet ${service}; then | |
echo "${service} previously enabled, reloading and restarting" | |
systemctl --user daemon-reload | |
systemctl --user restart ${service} | |
else | |
echo "enabling and starting ${service}" | |
systemctl --user enable ${service} | |
systemctl --user start ${service} | |
fi | |
systemctl --user status ${service} | |
done | |
} | |
## | |
# Rename the file based on date-time | |
# | |
# If the file does not matche the format YYYYMMDD_HHMMSS then we rewrite the filename based on | |
# either the EXIF datestamp or the file modified time, whichever is available. | |
# | |
function image_rename() | |
{ | |
if ! file "$@" | grep -qoP '^.+: \w+ image'; then | |
echo "${@} is not an image file" | |
return 1 | |
fi | |
local dirname="$(dirname "$@")" | |
local filename="$(basename "$@")" | |
local extension="${filename##*.}" | |
local newextension="${extension,,}" | |
filename="${filename%.*}" | |
[ "${newextension}" == 'jpeg' ] && newextension=jpg | |
datearray=( | |
$(sed 's/[_\ ]/\ /g' <<< "${filename}") | |
) | |
let i=0 | |
local contains_date=false | |
until ${contains_date}; do | |
date -d "${datearray[$i]}" &>/dev/null | |
[ $? -eq 0 ] && contains_date=true || contains_date=false | |
if [ $i -ge $((${#datearray[@]} - 1)) ]; then | |
break | |
fi | |
let i=$(($i+1)) | |
done | |
local date='' | |
if ${contains_date}; then | |
let i=$(($i - 1)) | |
date=$(sed 's/\ /_/g' <<< "${datearray[@]:${i}}") | |
else | |
date=$(exiftool "$@" | grep 'Date\/Time Original' | cut -d: -f2- | awk '{gsub(/^ /,"");gsub(/ /,"_");gsub(":","");print}') | |
if [ -z "${date}" ]; then | |
# fall back to stat as a last resort | |
date=$(stat -c %y "$@" | awk -F\. '{gsub(/ /,"_",$1); gsub(/[:-]/, "",$1); print $1;}') | |
fi | |
fi | |
#if [ "${filename}.${extension}" != "${date}.${newextension}" ]; then | |
mv "${dirname}/${filename}.${extension}" "${DARKTABLE}/${date}.${newextension}" | |
#fi | |
} | |
## | |
# Rename all image files found under a directory using the image_rename function above | |
# | |
function directory_rename() | |
{ | |
while read -d '' filename; do | |
image_rename "$filename" | |
done < <(find . -type f -print0) | |
} | |
## | |
# Bind function for the export directory | |
# | |
# Calls inotifywait with -m to continually monitor for new files | |
# | |
function inotify_output() | |
{ | |
inotifywait -m ${DARKTABLE} -e create -e moved_to | | |
while read d action filename; do | |
# wait for file to be closed then sleep for .1 seconds as darktable moves this file. | |
if [ "${action}" != 'MOVED_TO' ] ; then | |
[ -f "${DARKTABLE}/${filename}" ] && inotifywait -e close_write ${DARKTABLE}/${filename} | |
sleep .1 | |
fi | |
# We'll have another inotifywait command as a temporary file is moved so if the file | |
# is deleted, skip. | |
[ ! -f "${DARKTABLE}/${filename}" ] && continue | |
resize ${filename} | |
done | |
} | |
## | |
# Bind function for the input directory | |
# | |
# Calls inotifywait with -m to continually monitor for new files | |
# | |
function inotify_input() | |
{ | |
inotifywait -m ${INPUT_DIR} -e create -e moved_to | | |
while read d a filename; do | |
[ -f "${INPUT_DIR}/${filename}" ] && inotifywait -e close_write ${INPUT_DIR}/${filename} | |
input ${filename} | |
done | |
} | |
## | |
# print usage information | |
function usage() | |
{ | |
echo "Usage: dtcp.bash --[install|input|output|rename|rename-all]" | |
echo " --install - install script and dependencies" | |
echo " --input - start an inotifywait daemon monitoring the input folder" | |
echo " --output - start an inotifywait daemon monitoring the output folder" | |
echo " --rename <filename> - rename a single image file" | |
echo " --rename-all - renames all image files found under a directory" | |
} | |
## | |
# execute the script given the correct input arguments | |
action=$1 | |
case ${action} in | |
"--install") | |
install $0 | |
;; | |
"--input") | |
inotify_input | |
;; | |
"--output") | |
inotify_output | |
;; | |
"--rename") | |
image_rename "$2" | |
;; | |
"--rename-all") | |
directory_rename | |
;; | |
*) | |
usage | |
;; | |
esac |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment