#!/bin/bash

Version=1.03
Myname="${0##*/}"

:<<'DOC'
= renamefotos - rename all image/movie files in current dir by date/time

= Synopsis
renamefotos [options]	

== Options
-h,--help	print this help and exit
-H,--Help	print full documentation via less and exit
-V,--version	print version and exit
-f,--force	rename even if some files have no creation date
-d,--dry	do a dry run, i.e. don´t actually rename files

= Description
renamefotos renames all files in the current directory with the following
extensiions:

      .asf  .jpeg .mov  .mts  .tif 
      .avi  .jpg  .mp4  .png  .tiff

and their uppercase versions.
The extensions are converted to 3 lowercase characters (like JPEG → jpg,
tiff → tif).
The base names are converted to the format |yyyymmdd-hhmmss| as reported by
|exiftool|, using its |-datetimeoriginal|, |-createdate|, or |-gpsdatetime|
options, in that order. If renaming would generate an existing filename, an
extra character |a-z| is added. Files which lack date/time information in
their metadata will be renamed to 101.xxx, 102.xxx, ...
Base names that are already in that format are skipped here, unless you use
the |--all| option.
When one or more files don’t contain date/time information in their
metadata, you get a warning and you are asked to make a decision about what
to do with them:

1. go on
2. go on but set basename of dateless files in metadata title
3. skip dateless files
4. do nothing

 
= Author
[Wybo Dekker](wybodekker@me.com)

= Copyright
Released under the [GNU General Public License](www.gnu.org/copyleft/gpl.html)
DOC

REd='\e[38;5;9m' Mag='\e[38;5;5m' Nor='\e[0m'
    die() { local i; for i; do echo -e "$Myname: $REd$i$Nor"; done 1>&2; exit 1; }
   Warn() { local i; for i; do echo -e "$Myname: $Mag$i$Nor"; done 1>&2; }
helpsrt() { sed -n '/^= Synopsis/,/^= /p' "$0"|sed '1d;$d'; exit; }
helpall() { sed -n "/^:<<'DOC'$/,/^DOC/p" "$0"|sed '1d;$d'|
            less -P"$Myname-${Version/./·} (press h for help, q to quit)";exit; }

:<<'DOC' #---------------------------------------------------------------------
= excheck
synopsis:	 excheck executable1 [executable2...]
description:	check if all needed execs are there and getopt is GNU
globals used:	 BASH_VERSINFO
DOC
#-------------------------------------------------------------------------------
excheck() {
   local ok=true i missing=()
   ((BASH_VERSINFO>=4)) || die "Need bash version >= 4"
   for i; do 
      command -v "$i" > /dev/null && continue
      missing+=("$i")
      ok=false
   done
   $ok || die "Missing executables: ${missing[*]}"
   getopt -T 
   [[ $? -ne 4 ]] && die "Your getopt is not GNU"
}

:<<'DOC' #----------------------------------------------------------------------
= handle_options
synopsis:	 handle_options "$@"
description:	handle the options.
globals used:	 Myname Version
DOC
#-------------------------------------------------------------------------------
handle_options() {
   local options
   options=$(getopt \
      -n "$Myname" \
      -o hHVIfa \
      -l help,Help,version,force,all -- "$@"
   ) || exit 1
   eval set -- "$options"
   force=false
   while [ $# -gt 0 ]; do
      case $1 in
      (-h|--help)    # print this help and exit
		     helpsrt
		     ;;
      (-H|--Help)    # print full documentation via less and exit
		     helpall
		     ;;
      (-V|--version) # print version and exit
		     echo $Version
		     exit
		     ;;
      (-a|--all)     # rename even if already in date/time format
		     all=true
		     shift
		     ;;
      (-f|--force)   # rename even if some files have no creation date
		     force=true
		     shift
		     ;;
      (-I)           instscript "$Myname" ||
			die "the -I option is for developers only"
		     exit
		     ;;
      (--)           shift
		     break
		     ;;
      (*)            break
		     ;;
      esac
   done
}

:<<'DOC' #----------------------------------------------------------------------
= finddate
synopsis:	 finddate arg
description:	finddate tries, with |exiftool|‘s |-date| options, to find the
		creation date of the photo given in the argument.
		The date found is reported in the format |dddddddd-dddddd|. 
		If no date is found, reports an empty line.
globals used:	 none
DOC
#-------------------------------------------------------------------------------
finddate() {
   exiftool -datetimeoriginal -createdate -gpsdatetime -s3 -d "%Y%m%d-%H%M%S" "$1" |
   head -1
}

:<<'DOC' #----------------------------------------------------------------------
= exists
synopsis:	 exists targetname
description:	test if targetname already exists as a value in |fromto| array
globals used:	 fromto
DOC
#-------------------------------------------------------------------------------
exists() {
   local i
   for i in "${!fromto[@]}"; do 
      [[ $1 == "${fromto["$i"]}" ]] && return
   done
   return 1
}

:<<'DOC' #----------------------------------------------------------------------
= mkuniq
synopsis:	 mkuniq from to-base  to-ext
description:	set the value of fromto[$1] to $2.$3
globals used:	 Myname Version
DOC
#-------------------------------------------------------------------------------
mkuniq() {
   local j=0 to fr="$1" b="$2" e="$3" # from basename extension
   local app=({a..z})	# add a, b, ...to dups
   to="$random-$b.$e"
   while exists "$to"; do
      ((j>25)) && die "No files renamed" \
          "quitting because too many files have the same date ($base)"
      to="$random-$b${app[((j++))]}.$e"
   done
   fromto["$fr"]="$to"
   [[ $to =~ [a-z]\. ]] && ((same++))
}

excheck getopt exiftool cols
handle_options "$@"

declare -A fromto
while IFS='' read -r i; do
  i=${i#./}
  fromto["$i"]=''
done <<<"$(find ./ -maxdepth 1 -type f \( \
    -iname \*.jpg -o \
    -iname \*.jpeg -o \
    -iname \*.mov -o \
    -iname \*.avi -o \
    -iname \*.mp4 -o \
    -iname \*.asf -o \
    -iname \*.mts -o \
    -iname \*.tif -o \
    -iname \*.tiff -o \
    -iname \*.png \
  \))"

(( ${#fromto[@]} == 0 )) && die "No image/video files here"

ok=0
n=100		# date-less files renumbered 101, 102,,,
missing=()	# files that have no date
same=0		# no of files with repeating date/time
random=$(openssl rand -hex 10)

# loop through the files and:
# if the base is already in date/time format, don't change it.
# if not, if the date/time can be found replace the base with it.
# if not, set target to 101, 102, 103,...
# make the extension lower case
# change .jpeg to .jpg and .tiff to .tif
for i in "${!fromto[@]}"; do
   ext="${i##*.}"
   ext="${ext,,}"
   [[ $ext == jpeg ]] && ext=jpg
   [[ $ext == tiff ]] && ext=tif
   base=${i%.*}
   # if the base name is already in date/time format, it may have been assigned 
   # om the basis of other than exiftool information, so we do not change it. 
   # Still, the extension is corrected to 3 lower case characters.
   if [[ ! $all && $base =~ ^[[:digit:]]{8}-[[:digit:]]{6}[[:lower:]]?$ ]]; then
      mkuniq "$i" "$base" "$ext"
   else
      base=$(finddate "$i")
      if [[ -n $base ]]; then
         mkuniq "$i" "$base" "$ext"
      else
         missing+=("$i")
         ((n++))
         fromto["$i"]="$random-$n.$ext"
      fi
   fi
done

if ! $force && (( ${#missing[@]} > 0)); then
   Warn "The following files have no date/time information in their metadata:\n"
   printf '%s\n' "${missing[@]}" | sort | cols
   echo
   Warn "They will be renamed to 101.xxx, 102.xxx ... What do you want:" \
        "(type 1-4 or q to quit)"
   select i in	"go on" \
		"go on but set basename of dateless files in metadata title" \
		"skip dateless files" \
		"do nothing"
   do
      [[ -n $i || $REPLY == q ]] && break
   done
   case $REPLY in
   (1) ;;
   (2) for i in "${missing[@]}"; do
          exiftool -title="${i%.*}" -overwrite_original -q "$i"
       done
       ;;
   (3) for i in "${missing[@]}"; do
          unset fromto["$i"]
       done
       ;;
   (4|q) die "Nothing done"
       ;;
   esac
fi
for i in "${!fromto[@]}"; do mv "$i" "${fromto["$i"]}"; done
for i in "$random-"*; do mv "$i" "${i#$random-}"; done
