Automated Photo Categorization
Having a lot of photos not categorized could require some (a lot) of effort to get to a point where you end up with a situation fully based on a standard like <year>/<month>/<location>/<file>.
I decided to realize a very small, easy and maybe not so "clean" bash program that takes in input:
- the action to perform (copy files or create symlinks)
- the folder in which look for photos
- the folder in which create the organized structure
- how many levels in the source folder traverse
With all the information collected the program will go through each file found, analyze it and use an external reverse geocoding platform (thank you a lot OpenStreetMap) to fill the gap between raw gps coordinates and address.
Before even trying to use the program by yourself on any Linux platform, make sure you have installed:
- basename
- bash
- cp
- curl
- cut
- date
- exiftool
- find
- iconv
- jq
- ln
- mkdir
- numfmt
- readlink
- sed
- tr
- xargs
And now ... below the code 😉
#!/bin/bash
VERSION="0.1"
LANG=""
export LANG
oIFS=$IFS
IFS=$'\n'
export IFS
INPUTDIR=""
LEVELS=""
OUTPUTDIR=""
MODE=""
MODE="$1"
INPUTDIR="$2"
OUTPUTDIR="$3"
LEVELS="$4"
if [[ "$MODE" != "c" && "$MODE" != "s" ]]; then
echo "Mode is not set to a valid value (c|s)"
exit 1
fi
if [[ "$INPUTDIR" != "" && ! -d "$INPUTDIR" ]]; then
echo "Input dir '${INPUTDIR}' is not a valid directory"
exit 2
fi
if [[ "$OUTPUTDIR" != "" && ! -d "$OUTPUTDIR" ]]; then
echo "Output dir '${OUTPUTDIR}' is not a valid directory"
exit 3
fi
if [[ "$LEVELS" != "" && ! "$LEVELS" =~ ^[0-9]+$ ]]; then
echo "Nesting level is not a number"
exit 4
fi
[ "$LEVELS" = "" ] && LEVELS=9999
if [[ "$INPUTDIR" = "" && "$OUTPUTDIR" = "" ]]; then
echo ""
echo "Media - Image - Geo Organizer"
echo "ver: ${VERSION} - Davide Airaghi"
echo "Usage: "
echo "- command: media-img-time_geo.sh <MODE> <INDIR> <OUTDIR> <LEVELS>"
echo "- param <MODE> : c: copy file, s: create symlink"
echo "- param <INDIR> : directory containing images to classify"
echo "- param <OUTDIR>: into which directory copy & organize files"
echo "- param <LEVELS>: how many nesting levels consider (min: 1) while searching"
echo "Notes: "
echo "- file will be organized as <YEAR> / <MONTH> / <LOCALITY> / <FILE>"
echo "- <YEAR> and <MONTH> will be extracted by metadata, file name, file modifcation date"
echo "- <LOCALITY> will be extrated by GPS coordinates found in file metadata, whenever possible"
echo "- if no locality can be found, <FILE> will be put in <YEAR> / <MONTH>"
echo ""
exit 0
fi
if [[ "$INPUTDIR" = "" || "$OUTPUTDIR" = "" ]]; then
echo "Input directory and/or Output directory not provided"
exit 5
fi
declare -A fCOORDS
fLAT=""
fLNG=""
fLOC=""
fJSON=""
fDATE=""
fDEST=""
for FILE in $(find . -maxdepth 1 -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.webp" -o -iname "*.heic" \) 2>/dev/null) ;
#for FILE in 20201020-WA0000.jpg ;
do
FILE="$( echo $FILE | sed -e 's/^\.\///' )"
echo "Analyzing ${FILE} ..."
fLAT="$( exiftool -n -GPSLatitude -c '%.4f' $FILE 2>/dev/null | cut -d: -f2 | xargs | LANG=en numfmt --field=1- --format=%.4f --invalid=ignore )"
fLNG="$( exiftool -n -GPSLongitude -c '%.4f' $FILE 2>/dev/null | cut -d: -f2 | xargs | LANG=en numfmt --field=1- --format=%.4f --invalid=ignore )"
fDATE="$( exiftool -d "%Y/%m" -CreateDate $FILE 2>/dev/null | cut -d: -f2 | xargs )"
fLOC=""
echo -n "- Checking GPS Coords: "
if [[ $fLAT != "" && $fLNG != "" ]]; then
fKEY="${fLAT},${fLNG}"
_CACHED="(not cached)"
if [ "${fCOORDS[$fKEY]}" = "" ]; then
sleep 1.5
fJSON="$( curl "https://nominatim.openstreetmap.org/reverse?lat=${fLAT}&lon=${fLNG}&format=json" 2>/dev/null )"
_COUNTRY="$( echo "$fJSON" | jq .address.country 2>/dev/null )"
_CITY="$( echo "$fJSON" | jq .address.city 2>/dev/null)"
_VILLAGE="$( echo "$fJSON" | jq .address.village 2>/dev/null )"
_TOWN="$( echo "$fJSON" | jq .address.town 2>/dev/null )"
[[ "${_CITY}" = "null" || "${_CITY}" = "NULL" ]] && _CITY=""
[[ "${_TOWN}" = "null" || "${_TOWN}" = "NULL" ]] && _TOWN=""
[[ "${_VILLAGE}" = "null" || "${_VILLAGE}" = "NULL" ]] && _VILLAGE=""
[ "${_CITY}" = "" ] && _CITY="${_VILLAGE}"
[ "${_CITY}" = "" ] && _CITY="${_TOWN}"
fLOC="$( echo "${_COUNTRY}-${_CITY}" | tr . _ | tr -d '"' | tr ' ' '_' | tr "'" '_' | iconv -f utf-8 -t ascii//translit )"
fLOC="${fLOC^^}"
else
fLOC="${fCOORDS[$fKEY]}"
_CACHED="(cache)"
fi
fLOC=$( echo $fLOC | sed -e 's/^NULL//' | sed -e 's/-NULL$//' | grep -v -E '\-$' | xargs )
[ "${fLOC}" = "-" ] && fLOC=""
[ "${fLOC}" != "" ] && fCOORDS[$fKEY]="${fLOC}"
[ "${fLOC}" = "-" ] && echo "Not found"
[ "${fLOC}" != "" ] && echo "Found - ${fLAT};${fLNG};${fLOC};${_CACHED}"
else
echo "Not found"
fi
echo -n "- Checking Date & Time: "
# if empty date, trying the filename
if [ "${fDATE}" = "" ]; then
fDATE="$( basename $FILE | sed -e 's/^IMG.//g' | sed -e 's/^img.//g' | sed -e 's/^PIC.//g' | sed -e 's/^pic.//g' | sed -e 's/^VIDEO.//g' | sed -e 's/^video.//g' | sed -e 's/^VID.//g' | sed -e 's/^vid.//g' )"
fDATE="$( echo $fDATE | sed -e 's/-WA.*$//' | tr -d '-' | tr -d '_' | tr -d '.' )"
fDATE="$(date -d ${fDATE} +%Y/%m 2>/dev/null | xargs )"
fi
# if still empty date, using file attribute
if [ "${fDATE}" = "" ]; then
fDATE="$( exiftool -d "%Y/%m" -FileModifyDate $FILE 2>/dev/null | cut -d: -f2 | xargs )"
fi
[ "${fDATE}" = "" ] && echo "Not found"
[ "${fDATE}" != "" ] && echo "Found - ${fDATE}"
fDEST=`readlink -m ${OUTPUTDIR}/${fDATE}/${fLOC}/`
mkdir -p $fDEST 2>/dev/null >/dev/null
fDEST="${fDEST}/$(basename $FILE)"
case "$MODE" in
"c")
echo "- Copying file to ${fDEST}"
cp "$FILE" "${fDEST}"
;;
"s")
echo "- Absolute Symlinking file to ${fDEST}"
ln -s `readlink -f $FILE` "${fDEST}"
;;
*)
echo "- Skipping for unknow 'mode'"
;;
esac
done