levineuwirth.org/tools/photo-manifest.sh

91 lines
3.5 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# photo-manifest.sh — draft a manifest for tools/import-photos.sh.
#
# The batch importer takes a tab-separated manifest, which is the right input
# for photographs gathered over years from scattered directories — but only if
# something else writes the first draft. This does that: it finds the images,
# reads every capture date in ONE exiftool call, orders them chronologically,
# and emits a line per photograph with a date-stamped slug and an empty title.
#
# The output is meant to be edited: delete the lines you don't want, fill in
# the titles you do. Nothing is imported and nothing is touched — it only
# reads, and writes to stdout.
#
# Usage:
# tools/photo-manifest.sh <dir> --prefix undergrad > /tmp/undergrad.tsv
# $EDITOR /tmp/undergrad.tsv
# tools/import-photos.sh --manifest /tmp/undergrad.tsv --series undergrad
#
# Options:
# --prefix S slug prefix (required): S-YYYYMMDD-NN
# --recursive descend into subdirectories
#
# Slugs are date-stamped rather than sequential because they become URLs and
# ought to survive the set changing: deleting a line from the middle of the
# manifest must not renumber everything after it. A photograph keeps its slug
# no matter what else you decide to keep.
set -uo pipefail
DIR=""; PREFIX=""; RECURSIVE=false
while [ "$#" -gt 0 ]; do
case "$1" in
--prefix) PREFIX="$2"; shift 2 ;;
--recursive) RECURSIVE=true; shift ;;
-h|--help) sed -n '2,30p' "$0"; exit 0 ;;
-*) echo "unknown option: $1" >&2; exit 2 ;;
*) DIR="$1"; shift ;;
esac
done
[ -n "$DIR" ] || { echo "photo-manifest: need a directory" >&2; exit 2; }
[ -d "$DIR" ] || { echo "photo-manifest: no such directory: $DIR" >&2; exit 2; }
[ -n "$PREFIX" ] || { echo "photo-manifest: --prefix is required" >&2; exit 2; }
[[ "$PREFIX" =~ ^[a-z0-9-]+$ ]] || { echo "photo-manifest: invalid prefix '$PREFIX'" >&2; exit 2; }
command -v exiftool >/dev/null 2>&1 || { echo "photo-manifest: exiftool required" >&2; exit 1; }
depth=(); $RECURSIVE || depth=(-maxdepth 1)
mapfile -d '' -t FILES < <(
find "$DIR" "${depth[@]}" -type f \
\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.heic' \) \
-print0 | sort -z
)
if [ "${#FILES[@]}" -eq 0 ]; then
echo "photo-manifest: no images found in $DIR" >&2
exit 1
fi
# One exiftool call for the whole directory. Per-file it would be ~0.3 s each,
# which is the entire reason import-photos.sh exists.
exiftool -q -json -DateTimeOriginal -CreateDate -FileModifyDate "${FILES[@]}" \
| PREFIX="$PREFIX" python3 -c '
import json, os, sys, collections
rows = json.load(sys.stdin)
prefix = os.environ["PREFIX"]
def when(r):
# DateTimeOriginal is the shutter. CreateDate is a decent second. File
# mtime is a last resort and often lies — a copied file carries the date
# of the copy — but an approximate order beats no order.
for key in ("DateTimeOriginal", "CreateDate", "FileModifyDate"):
v = r.get(key)
if isinstance(v, str) and len(v) >= 10 and v[:4].isdigit():
return v[:10].replace(":", "")
return "00000000"
rows.sort(key=lambda r: (when(r), r.get("SourceFile", "")))
seq = collections.Counter()
print("# path\tslug\ttitle — delete unwanted lines, fill in titles")
print(f"# {len(rows)} photographs, in capture order")
for r in rows:
src = r.get("SourceFile")
if not src:
continue
day = when(r)
seq[day] += 1
print(f"{src}\t{prefix}-{day}-{seq[day]:02d}\t")
'