#!/bin/bash

vers=2026.03.31.A
# make a media web page index in specified folder
# by N0ZYC

# TO DO:

# move folder athumbs into folder they are for (call them folderthumb.x ?)
# auto-convert wrong size bannerpic (should be 250 tall) or athumb (should be 130 tall)


# set to 1 to enable basic debugging
switch_debug=

# set to 1 to enable Finder comments debugging
switch_debug_comment=

# set to 1 to enable cache debugging
switch_debug_cache=

# set to 1 to enable movie thumbnail extraction debugging
switch_debug_extract=

# set to 1 to enable alias debugging
switch_debug_alias=

# set to 1 to refresh cache files
switch_rebuild_caches=

# set to 1 to disable creating/removing of cache files and replacing of index files
switch_dryrun=

# set to 1 to rebuild thumbs that have portrait dimensions
switch_rebuild_portrait_thumbs=1





################################################################################
################################################################################
##
##  CONFIGURE
##
################################################################################
################################################################################


########################################
##  FILES AND FOLDERS
########################################

# temp files
temp_file=".temp"
new_index=".new_index"
plist="webloc.plist"

# index file to make
index_file="index.html"

# if present, render this text at the top in large/bold
bannertext="bannertext.txt"

# if present, render this banner picture below the banner text
bannerpic="bannerpic"  # any extension

# folder that full size images (not max) are stored in
max_folder="max"

# folder that full size images (not max) are stored in
full_folder="full"

# folder thumbnails are stored in
thumb_folder="thumb"

# alias thumbnails are stored in
athumb_folder="athumb"

# cache file (holds file sizes, image/video dimensions, and Finder comments
cache_file=".cache.txt"

# videos are probably better stored in a separate subfolder but placed in the parent folder video gallery
videos_folder="videos"



########################################
##  MARKER FILES
########################################

# there are also other marker files used my the recursive reindexing script that calls us

# marker to stop recursively reindexing
do_not_index_file=".do not index"

# marker to NOT arrange images into a gallery
noigallery_file=".noigallery"

# marker to NOT arrange videos into a gallery
novgallery_file=".novgallery"

# marker in folder to not create a link to (still syncs)
hide_file=".hide"

# flag to not add a "back" link
do_not_back_file=".do not back"

# flag to not try to borrow an alias's athumb
do_ignore_alias_athumb=".do ignore alias athumb"

# flag to not adjust max image dimensions
do_not_redim=".do not redim"




########################################
##  CONSTANTS
########################################

# add a size notice on large clickable links
size_warn_limit_kb=250

# define full image width/height where resizing down will occur
full_vlimit=1281
full_hlimit=1601

# image thumbnail size
image_thumbnail_width=200  # can be wider
image_thumbnail_height=130

# video thumbnail size
video_thumbnail_width=200  # can be wider
video_thumbnail_height=150

# default time index into video to extract thumbnail at (will extract from middle of video if total video size is smaller than 2x this)
default_video_index=10

# make image or video gallery if there are at least this many images / videos available
gallery_minfiles=3

# judge a video or image gallery thumb to be portrait orientation if its height is this percent more more than its width
portrait_thumb_pct=130



########################################
##  SWITCHES
########################################

# flags for what to open in new tabs
open_bookmarks_in_new_tab=1
open_aliases_in_new_tab=1
#open_folder_in_new_tab=1
open_documents_in_new_tab=1
#open_maxfolder_in_new_tab=1





################################################################################
##
##  FUNCTIONS
##
################################################################################


########################################
##  ABORT
########################################

# critical error encountered abort at specified line

abort () {

line=$1
msg=$2
cmd=$3
if [ -z "$cmd" ] ; then
  echo "ABORT at line $((line-1)): $msg" 1>&2
else
  echo "ABORT at line $((line-1)) trying to $msg with CMD: $cmd" 1>&2
fi
exit 1

}




########################################
##  ARMOR FOR APPLESCRIPT
########################################

# escape quotes for applescript strings and paths

armor_for_applescript () {

echo -n "$1" | sed 's/\"/\\\"/g'

}




########################################
##  ALIAS TO LINK
########################################

# find the relative html path pointed to by the specified modern Mac OS alias file
# don't cache this because it'd be just as expensive to try to catch changed paths

alias_to_link () {
local path link current_dir alias_dir source

source=$1

# alias_to_link has no cache so is always slow
#info "alias_to_link \"$source\""

path=$(alias_to_path "$source")  
if [ -z "$path" ] ; then
  abort $LINENO "passed blank alias path"
fi
if [ "$switch_debug_alias" ] ; then
  echo "ALIAS path=\"$path\"" 1>&2
fi
if [ -z "${path##*/}" ] ; then
  path=${path:0:${#path}-1}  # trim off trailing slash
  if [ "$switch_debug_alias" ] ; then
    echo "ALIAS (trim) path=\"$path\"" 1>&2
  fi
fi
tdir=${path%/*}
tfile=${path##*/}
current_dir=$(pwd)
alias_dir="$tdir"
if [ "$switch_debug_alias" ] ; then
  echo "ALIAS        tdir=\"$tdir\"" 1>&2
  echo "ALIAS       tfile=\"$tfile\"" 1>&2
  echo "ALIAS current_dir=\"$current_dir\"" 1>&2
  echo "ALIAS   alias_dir=\"$alias_dir\"" 1>&2
  echo "ALIAS        path=\"$path\"" 1>&2
fi

# check for alias is child of current folder
if [ "${path:0:${#current_dir}+1}" == "${current_dir}/" ] ; then
  if [ "$switch_debug_alias" ] ; then
    echo "ALIAS (child return) \".${path:${#current_dir}}\"" 1>&2
  fi
  echo ".${path:${#current_dir}}"
  return
fi
if [ "$switch_debug_alias" ] ; then
  echo "ALIAS (not child)" 1>&2
fi


#ALIAS        tdir="/Users/nfisher/DATA_XFER/www/N0ZYC/power/powerpoles/PP series (PP15,30,45)/powerpole distribution blocks and harnesses"
#ALIAS       tfile="PowerPole distribution blocks"
#ALIAS current_dir="/Users/nfisher/DATA_XFER/www/N0ZYC/power/powerpoles"
#ALIAS   alias_dir="/Users/nfisher/DATA_XFER/www/N0ZYC/power/powerpoles/PP series (PP15,30,45)/powerpole distribution blocks and harnesses"
#ALIAS        path="/Users/nfisher/DATA_XFER/www/N0ZYC/power/powerpoles/PP series (PP15,30,45)/powerpole distribution blocks and harnesses/PowerPole distribution blocks"
#ALIAS (STOP HERE)




# build up forward link from common fork to target
if [ "${current_dir%%/*}" != "${alias_dir%%/*}" ] ; then
  if [ "$switch_debug_alias" ] ; then
    echo "ALIAS (NO forward)" 1>&2
  fi
fi
while [ "${current_dir%%/*}" == "${alias_dir%%/*}" ] ; do
  current_dir=${current_dir#*/}
  oa="$alias_dir"
  alias_dir=${alias_dir#*/}
  if [ "$switch_debug_alias" ] ; then
    echo "ALIAS (forward) current_dir=\"$current_dir\"" 1>&2
    echo "ALIAS (forward)          oa=\"$oa\"" 1>&2
    echo "ALIAS (forward)   alias_dir=\"$alias_dir\"" 1>&2
  fi
  if [ "$alias_dir" == "$oa" ] ; then
    alias_dir="."
    if [ "$switch_debug_alias" ] ; then
      echo "ALIAS (forward OA) alias_dir=\"$alias_dir\"" 1>&2
    fi
  fi
done

# build up backup from current folder to common fork
link=
c= 
if [ "$current_dir" == "$c" ] ; then
  if [ "$switch_debug_alias" ] ; then
    echo "ALIAS (NO backup)" 1>&2
  fi
fi
while [ "$current_dir" != "$c" ] ; do
  link="../$link"
  c="$current_dir"
  current_dir=${current_dir#*/}
  if [ "$switch_debug_alias" ] ; then
    echo "ALIAS (backup)        link=\"$link\"" 1>&2
    echo "ALIAS (backup)           c=\"$c\"" 1>&2
    echo "ALIAS (backup) current_dir=\"$current_dir\"" 1>&2
  fi
done

# return result to caller
if [ "$switch_debug_alias" ] ; then
  echo "ALIAS (return)      link=\"$link\"" 1>&2
  echo "ALIAS (return) alias_dir=\"$alias_dir\"" 1>&2
  echo "ALIAS (return)     tfile=\"$tfile\"" 1>&2
  echo "ALIAS (return)    return=\"${link}${alias_dir}/${tfile}\"" 1>&2
fi
echo "${link}${alias_dir}/${tfile}"

}



########################################
##  ALIAS TO PATH
########################################

# extract the unix path pointed to by the specified modern Mac OS alias file
# don't cache this because it'd be just as expensive to try to catch changed paths

alias_to_path () {
local source x item_name item_parent item_path line_1 line_2 line_3 line_4 line_5 line_6 linksource

source=$1

# alias_to_path has no cache so is always slow
#info "alias_to_path \"$source\""

# verify file exists
if ! [ -f "$source" ] ; then
  abort $LINENO "alias file \"$source\" not found"
fi  

# verify it's a modern alias file by confirming the file magic
x=$(dd if="$source" bs=1 count=16 2> /dev/null | xxd -u -p)  # "book....mark...."
if [ "$x" != "626F6F6B000000006D61726B00000000" ] ; then
  # return blank and an error code
  return 1
fi

# craft and submit an applescript to resolve the alias
item_name=$(basename "$source")
item_parent=$(dirname "$source")
if [ "$item_parent" == "." ] ; then
  item_parent=$(PWD)
fi
item_path="${item_parent}/${item_name}"
line_1='tell application "Finder"'
line_2='set theItem to (POSIX file "'${item_path}'") as alias'
line_3='if the kind of theItem is "alias" then'
line_4='get the posix path of (original item of theItem as text)'
line_5='end if'
line_6='end tell'
linksource=$(osascript -e "$line_1" -e "$line_2" -e "$line_3" -e "$line_4" -e "$line_5" -e "$line_6")

if [ $? != 0 ] ; then
  warn "broken alias at \"${root}/$source\"" 1>&2
  return 1
elif [ -z "$linksource" ] ; then
  echo "possible resource fork alias at \"${root}/$source\"" 1>&2
  return 1
fi

# return result to caller
echo "$linksource"

}



########################################
##  ARMOR TEXT
########################################

# convert a user-generated text string into a web-safe string by armoring it for HTML text formatting

#   change & to &amp:
#   change < to &lt;
#   change > to &gt;
#   change " to &quot;
#   change ' to &#039;

# also adds <BR>'s to linefeesds

# supply "1" for parameter 2 to protect select HTML structures from the armoring process (this can be expensive)

armortext () {
local msg n i k

if [ -z "$2" ] ; then  # this text doesn't require html protection, just armor it all
  # don't need to protect html
  echo -n "$1" | tr '\n' $'\x1E' | sed -e 's/\&/\x1F/g' -e "s/</\&lt;/g" -e "s/>/\&gt;/g" -e "s/'/\&#039;/g" -e "s/\"/\&quot;/g" -e 's/\x1F/\&amp;/g' -e 's/\x1E/\<BR\>\n/g'
#  echo -n "$1" | tr '\n' $'\x1E' | sed -e 's/\&/\x1F/g' -e "s/</\&lt;/g" -e "s/>/\&gt;/g" -e "s/'/\&#039;/g" -e "s/\"/\&quot;/g" -e 's/\x1F/\&amp;/g' -e 's/\x1E/\<BR\>_/g'
  return
fi

# init key/html array
n=0

# armor linefeeds inside MONO blocks

# merge message into single line by changing linefeeds to \x1E
msg=$(echo -n "$1" | tr '\n' $'\x1E')
monokey=""

while true ; do

  #clear
  #n=0
  #msg=$(cat x | tr '\n' $'\x1E')

  # generate uppercase message for shell sub matching
  #umsg=$(echo "$msg" | tr 'a-z' 'A-Z')
  umsg=$(echo "$msg" | tr '[:lower:]' '[:upper:]')

  # find first <mono> tag
  msg_leftu=${umsg%%<MONO>*}  # all text before first <mono>"
  if [ ${#msg_leftu} == ${#umsg} ] ; then
    # opening <mono> tag not found
    break
  fi
  msg_left=${msg:0:${#msg_leftu}}

#echo "OPENING MONO TAG FOUND ${#msg_left} != ${#umsg} ======================================================" 1>&2
#echo "umsg = \"$umsg\"" | tr $'\x1e' '_' 1>&2

#echo "======================================================" 1>&2
#echo "msg_left = \"$msg_left\"" | tr $'\x1e' '_' 1>&2
#echo "======================================================" 1>&2
#echo 1>&2
#echo 1>&2
#echo 1>&2
#echo 1>&2
#echo 1>&2

  # find first </mono> tag
  msg_rightu=${umsg#*</MONO>}  # all text after first </mono>"  (may include another mono tag pair)
  if [ ${#msg_rightu} == ${#umsg} ] ; then
    # closing </mono> tag not found
    assert $LINENO "search for closing MONO tag" "msg##*</MONO>"
  fi
  msg_right=${msg:${#msg}-${#msg_rightu}}

#echo "======================================================" 1>&2
#echo "msg = \"$msg\"" | tr $'\x1e' '_' 1>&2
#echo "======================================================" 1>&2
#echo "umsg = \"$umsg\"" | tr $'\x1e' '_' 1>&2
#echo "======================================================" 1>&2
#echo "msg_left = \"$msg_left\"" | tr $'\x1e' '_' 1>&2
#echo "======================================================" 1>&2
#echo "msg_right = \"$msg_right\"" | tr $'\x1e' '_' 1>&2
#echo "======================================================" 1>&2
#echo " #msg=${#msg}, cut at $((${#msg_left}+6)) length $((${#msg}-${#msg_left}-6-${#msg_right}-7))" 1>&2
#echo "======================================================" 1>&2


  # extract the mono block
  msg_mono=${msg:${#msg_left}+6:${#msg}-${#msg_left}-6-${#msg_right}-7}

#echo "======================================================" 1>&2
#echo "MSG_MONO: \"$msg_mono\"" 1>&2
#echo "======================================================" 1>&2

  # create sub keys and shift sub keys into left/right
  kkey[n]="SUB${n}KEY"
  khtml[n]="<TT><PRE>"
  msg_left="${msg_left}${kkey[n]}"
  ((n++))
  kkey[n]="SUB${n}KEY"
  khtml[n]="</PRE></TT>"
  msg_right="${kkey[n]}${msg_right}"
  ((n++))

  # armor linefeeds inside the mono block
  if [ -z "$monokey" ] ; then
    # create the monokey
    kkey[n]="SUB${n}KEY"
    #khtml[n]=$'\n'
    khtml[n]=$'\x1E'
    #khtml[n]="(LF)"
    monokey=${kkey[n]}
    #monokey=ABC
    ((n++))
  fi
  msg_mono=$(echo "$msg_mono" | sed s/$'\x1E'/${monokey}/g)

  # update msg and umsg
#  echo "msg = \"$msg\"" | tr $'\x1e' '_' 1>&2
#  echo "msg_left = \"$msg_left\"" | tr $'\x1e' '_' 1>&2
#  echo "msg_right = \"$msg_right\"" | tr $'\x1e' '_' 1>&2
#  echo "msg_mono = \"$msg_mono\"" | tr $'\x1e' '_' 1>&2
  msg="${msg_left}${msg_mono}${msg_right}"
#  echo "msg = \"$msg\"" | tr $'\x1e' '_' 1>&2
#  echo 1>&2
#echo "======================================================" 1>&2
#echo "======================================================" 1>&2
#echo "======================================================" 1>&2
  

done

# split message back into multiple lines
msg=$(echo -n "$msg" | tr $'\x1E' '\n')

#echo "======================================================" 1>&2
#echo "MSG: $msg" 1>&2
#echo "======================================================" 1>&2


# protect specific html by substituting it with a unique key
#msg=$1
debug "WILL PROTECT HTML"
while true ; do

  # substitute <A HREF="xxx">yyy</A> html blocks
  #khtml[n]=$(echo "$msg" | grep -i -m 1 -o "<A HREF=\"mailto:[^\"]*\">[^>]*>")  # <A HREF="mailto:pres@w0mg.net?subject=I want to operate on Field Day">Al (KB0VGG)</A>
  #khtml[n]=$(echo "$msg" | grep -i -m 1 -o "<A HREF=\"[^\"]*\">[^<]*</A>")  # <A HREF="mailto:pres@w0mg.net?subject=I want to operate on Field Day">Al (KB0VGG)</A>  (works for mailto:, https:// etc)
  # -m 1 doens't work on the initial string, (bash grep bug?) so I have to tack on another one
  khtml[n]=$(echo "$msg" | grep -i -m 1 -o "<A HREF=\"[^\"]*\">[^<]*</A>" | grep -i -m 1 "</A>")  # <A HREF="mailto:pres@w0mg.net?subject=I want to operate on Field Day">Al (KB0VGG)</A>  (works for mailto:, https:// etc)


  if [ -n "${khtml[n]}" ] ; then  # found one
    # create a new key
    kkey[n]="SUB${n}KEY"
    debug "PROTECTING \"${khtml[n]}\" with \"${kkey[n]}\""
    # replace the html with the key
    #msg=$(echo "$msg" | sed "s/<[Aa] [Hh][Rr][Ee][Ff]=\"[^\"]*\">[^<]*<\/[Aa]>/${kkey[n]}/g")  # Please contact SUB1REF if you intend to operate, so we can work on scheduling to keep the radios active.</font>
    msg=$(echo "$msg" | sed "1,/<[Aa] [Hh][Rr][Ee][Ff]=\"[^\"]*\">[^<]*<\/[Aa]>/s/<[Aa] [Hh][Rr][Ee][Ff]=\"[^\"]*\">[^<]*<\/[Aa]>/${kkey[n]}/")  # Please contact SUB1REF if you intend to operate, so we can work on scheduling to keep the radios active.</font>
    debug "MSG=\"$msg\""

    # maybe adjust html for new window
    if [ $open_bookmarks_in_new_tab ] ; then
      # khtml[n]="<A HREF="https://w0yl.groups.io/g/main">Groups.IO</A>"
      tag="${khtml[n]#*>}"  # Groups.IO</A>  
      tag=${tag%<*}  # Groups.IO
      khtml[n]="${khtml[n]:0:${#khtml[n]}-${#tag}-5}"  # <A HREF="https://w0yl.groups.io/g/main"
      khtml[n]="${khtml[n]} ALT=\"${tag}\" TARGET=\"_blank\">${tag}</A>"  # <A HREF="https://w0yl.groups.io/g/main" ALT="click to open in new tab/window" TARGET="_blank">Groups.IO</A>
    fi

    # save key and search again
    ((n++))
    continue
  fi

  # substitute <B> html tags
  khtml[n]=$(echo "$msg" | grep -i -m 1 -o "<B>" | head -n1)  # <B>bad</B>
  if [ -n "${khtml[n]}" ] ; then  # found one
    # create a new key
    kkey[n]="SUB${n}KEY"
    # replace the html with the key
    msg=$(echo "$msg" | sed "s/<[Bb]>/${kkey[n]}/g")  # SUB1REFbad</B>
    # save key and search again
    ((n++))
    continue
  fi

  # substitute </B> html tags
  khtml[n]=$(echo "$msg" | grep -i -m 1 -o "</B>" | head -n1)  # <B>bad</B>
  if [ -n "${khtml[n]}" ] ; then  # found one
    # create a new key
    kkey[n]="SUB${n}KEY"
    debug "PROTECTING \"${khtml[n]}\" with \"${kkey[n]}\""
    # replace the html with the key
    msg=$(echo "$msg" | sed "s/<\/[Bb]>/${kkey[n]}/g")  # <B>badSUB1REF
    # save key and search again
    ((n++))
    continue
  fi

  # substitute <I> html tags
  khtml[n]=$(echo "$msg" | grep -i -m 1 -o "<I>" | head -n1)  # <I>good</I>
  if [ -n "${khtml[n]}" ] ; then  # found one
    # create a new key
    kkey[n]="SUB${n}KEY"
    debug "PROTECTING \"${khtml[n]}\" with \"${kkey[n]}\""
    # replace the html with the key
    msg=$(echo "$msg" | sed "s/<[Ii]>/${kkey[n]}/g")  # SUB1REFgood</I>
    # save key and search again
    ((n++))
    continue
  fi

  # substitute </I> html tags
  khtml[n]=$(echo "$msg" | grep -i -m 1 -o "</I>" | head -n1)  # <I>good</I>
  if [ -n "${khtml[n]}" ] ; then  # found one
    # create a new key
    kkey[n]="SUB${n}KEY"
    debug "PROTECTING \"${khtml[n]}\" with \"${kkey[n]}\""
    # replace the html with the key
    msg=$(echo "$msg" | sed "s/<\/[Ii]>/${kkey[n]}/g")  # <I>goodSUB1REF
    # save key and search again
    ((n++))
    continue
  fi

  # substitute <TT> html tags
  khtml[n]=$(echo "$msg" | grep -i -m 1 -o "<TT>" | head -n1)  # <TT>good</TT>
  if [ -n "${khtml[n]}" ] ; then  # found one
    # create a new key
    kkey[n]="SUB${n}KEY"
    debug "PROTECTING \"${khtml[n]}\" with \"${kkey[n]}\""
    # replace the html with the key
    msg=$(echo "$msg" | sed "s/<[Tt][Tt]>/${kkey[n]}/g")  # SUB1REFgood</TT>
    # save key and search again
    ((n++))
    continue
  fi

  # substitute </PRE> html tags
  khtml[n]=$(echo "$msg" | grep -i -m 1 -o "</TT>" | head -n1)  # <TT>good</TT>
  if [ -n "${khtml[n]}" ] ; then  # found one
    # create a new key
    kkey[n]="SUB${n}KEY"
    debug "PROTECTING \"${khtml[n]}\" with \"${kkey[n]}\""
    # replace the html with the key
    msg=$(echo "$msg" | sed "s/<\/[Tt][Tt]>/${kkey[n]}/g")  # <TT>goodSUB1REF
    # save key and search again
    ((n++))
    continue
  fi

  # substitute <PRE> html tags
  khtml[n]=$(echo "$msg" | grep -i -m 1 -o "<PRE>" | head -n1)  # <PRE>good</PRE>
  if [ -n "${khtml[n]}" ] ; then  # found one
    # create a new key
    kkey[n]="SUB${n}KEY"
    debug "PROTECTING \"${khtml[n]}\" with \"${kkey[n]}\""
    # replace the html with the key
    msg=$(echo "$msg" | sed "s/<[Pp][Rr][Ee]>/${kkey[n]}/g")  # SUB1REFgood</I>
    # save key and search again
    ((n++))
    continue
  fi

  # substitute </PRE> html tags
  khtml[n]=$(echo "$msg" | grep -i -m 1 -o "</PRE>" | head -n1)  # <PRE>good</PRE>
  if [ -n "${khtml[n]}" ] ; then  # found one
    # create a new key
    kkey[n]="SUB${n}KEY"
    debug "PROTECTING \"${khtml[n]}\" with \"${kkey[n]}\""
    # replace the html with the key
    msg=$(echo "$msg" | sed "s/<\/[Pp][Rr][Ee]>/${kkey[n]}/g")  # <I>goodSUB1REF
    # save key and search again
    ((n++))
    continue
  fi



  # substitute &nbsp; html tags
  khtml[n]=$(echo "$msg" | grep -m 1 -o "&nbsp;" | head -n1)  # this&nbsp;&nbsp;spot
  if [ -n "${khtml[n]}" ] ; then  # found one
    # create a new key
    kkey[n]="SUB${n}KEY"
    debug "PROTECTING \"${khtml[n]}\" with \"${kkey[n]}\""
    # replace the html with the key
    msg=$(echo "$msg" | sed "s/&nbsp;/${kkey[n]}/g")  # thisSUB1REFSUB1REFspot
    # save key and search again
    ((n++))
    continue
  fi

  # substitute <greencheck> html tags
  khtml[n]=$(echo "$msg" | grep -i -m 1 -o "<greencheck>" | head -n1)  # <greencheck>
  if [ -n "${khtml[n]}" ] ; then  # found one
    # create a new key
    kkey[n]="SUB${n}KEY"
    debug "PROTECTING \"${khtml[n]}\" with \"${kkey[n]}\""
    # replace the html with the key
    msg=$(echo "$msg" | sed "s/<[Gg][Rr][Ee][Ee][Nn][Cc][Hh][Ee][Cc][Kk]>/${kkey[n]}/g")  # <SUB1REF>
    #khtml[n]="my green check box"
#	khtml[n]='<div style="border: 9px solid green; width: 0px; height: 0px; display: inline-block; margin-right: 4px;"></div>'
#	khtml[n]='<div style="border: 9px solid green; display: inline-block; margin-right: 4px;"></div>'
#   khtml[n]='<div style="border: 1px solid black; width: 14px; height: 14px; display: inline-block; margin-left: 4px; margin-right: 4px;"><div style="border: 1px solid white; width: 12px; height: 12px; display: inline-block;"><div style="border: 6px solid green; display: inline-block; margin-right: 4px;"></div></div></div>'
#   khtml[n]='<div style="border: 1px solid black; width: 14px; height: 14px; display: inline-block; margin-left: 4px; margin-right: 4px;"><div style="border: 1px solid white; width: 12px; height: 12px; display: inline-block;"><div style="border: 6px solid lime; display: inline-block; margin-right: 4px;"></div></div></div>'
    khtml[n]='<div style="border: 1px solid black; width: 14px; height: 14px; display: inline-block; margin-left: 4px; margin-right: 4px;"><div style="border: 2px solid white; width: 10px; height: 10px; display: inline-block;"><div style="border: 5px solid lime; display: inline-block;"></div></div></div>'
    # save key and search again
    ((n++))
    continue
  fi

  # substitute <yellowcheck> html tags
  khtml[n]=$(echo "$msg" | grep -i -m 1 -o "<yellowcheck>" | head -n1)  # <yellowcheck>
  if [ -n "${khtml[n]}" ] ; then  # found one
    # create a new key
    kkey[n]="SUB${n}KEY"
    debug "PROTECTING \"${khtml[n]}\" with \"${kkey[n]}\""
    # replace the html with the key
    msg=$(echo "$msg" | sed "s/<[Yy][Ee][Ll][Ll][Oo][Ww][Cc][Hh][Ee][Cc][Kk]>/${kkey[n]}/g")  # <SUB1REF>
	#khtml[n]="my green check box"
#   khtml[n]='<div style="border: 4px dotted red; width: 10px; height: 10px; display: inline-block; margin-right: 4px;"></div>'
#   khtml[n]='<div style="border: 1px solid black; width: 14px; height: 14px; display: inline-block; margin-right: 4px;"></div>'
#   khtml[n]='<div style="border: 1px solid black; width: 14px; height: 14px; display: inline-block; margin-left: 4px; margin-right: 4px;"><div style="border: 1px solid white; width: 12px; height: 12px; display: inline-block;"><div style="border: 6px solid yellow; display: inline-block; margin-right: 4px;"></div></div></div>'
#   khtml[n]='<div style="border: 1px solid black; width: 14px; height: 14px; display: inline-block; margin-left: 4px; margin-right: 4px;"><div style="border: 2px solid white; width: 10px; height: 10px; display: inline-block;"><div style="border: 5px solid yellow; display: inline-block;"></div></div></div>'
    khtml[n]='<div style="border: 1px solid black; width: 14px; height: 14px; display: inline-block; margin-left: 4px; margin-right: 4px;"><div style="border: 2px solid white; width: 10px; height: 10px; display: inline-block;"><div style="border: 5px solid gold; display: inline-block;"></div></div></div>'
    # save key and search again
    ((n++))
    continue
  fi

  # substitute <redcheck> html tags
  khtml[n]=$(echo "$msg" | grep -i -m 1 -o "<redcheck>" | head -n1)  # <redcheck>
  if [ -n "${khtml[n]}" ] ; then  # found one
    # create a new key
    kkey[n]="SUB${n}KEY"
    debug "PROTECTING \"${khtml[n]}\" with \"${kkey[n]}\""
    # replace the html with the key
    msg=$(echo "$msg" | sed "s/<[Rr][Ee][Dd][Cc][Hh][Ee][Cc][Kk]>/${kkey[n]}/g")  # <SUB1REF>
	#khtml[n]="my red check box"
#   khtml[n]='<div style="border: 4px solid red; width: 10px; height: 10px; display: inline-block; margin-right: 4px;"></div>'
    khtml[n]='<div style="border: 1px solid black; width: 14px; height: 14px; display: inline-block; margin-left: 4px; margin-right: 4px;"><div style="border: 1px solid white; width: 12px; height: 12px; display: inline-block;"><div style="border: 6px solid red; display: inline-block; margin-right: 4px;"></div></div></div>'
#   khtml[n]='<div style="border: 1px solid black; width: 14px; height: 14px; display: inline-block; margin-left: 4px; margin-right: 4px;"><div style="border: 2px solid white; width: 10px; height: 10px; display: inline-block;"><div style="border: 5px solid red; display: inline-block;"></div></div></div>'
    # save key and search again
    ((n++))
    continue
  fi

  # substitute <bluecheck> html tags
  khtml[n]=$(echo "$msg" | grep -i -m 1 -o "<bluecheck>" | head -n1)  # <redcheck>
  if [ -n "${khtml[n]}" ] ; then  # found one
    # create a new key
    kkey[n]="SUB${n}KEY"
    debug "PROTECTING \"${khtml[n]}\" with \"${kkey[n]}\""
    # replace the html with the key
    msg=$(echo "$msg" | sed "s/<[Bb][Ll][Uu][Ee][Cc][Hh][Ee][Cc][Kk]>/${kkey[n]}/g")  # <SUB1REF>
	#khtml[n]="my red check box"
#   khtml[n]='<div style="border: 4px solid red; width: 10px; height: 10px; display: inline-block; margin-right: 4px;"></div>'
    khtml[n]='<div style="border: 1px solid black; width: 14px; height: 14px; display: inline-block; margin-left: 4px; margin-right: 4px;"><div style="border: 1px solid white; width: 12px; height: 12px; display: inline-block;"><div style="border: 6px solid DeepSkyBlue; display: inline-block; margin-right: 4px;"></div></div></div>'
#   khtml[n]='<div style="border: 1px solid black; width: 14px; height: 14px; display: inline-block; margin-left: 4px; margin-right: 4px;"><div style="border: 2px solid white; width: 10px; height: 10px; display: inline-block;"><div style="border: 5px solid red; display: inline-block;"></div></div></div>'
    # save key and search again
    ((n++))
    continue
  fi

  # substitute <greycheck> html tags
  khtml[n]=$(echo "$msg" | grep -i -m 1 -o "<greycheck>" | head -n1)  # <redcheck>
  if [ -n "${khtml[n]}" ] ; then  # found one
    # create a new key
    kkey[n]="SUB${n}KEY"
    debug "PROTECTING \"${khtml[n]}\" with \"${kkey[n]}\""
    # replace the html with the key
    msg=$(echo "$msg" | sed "s/<[Gg][Rr][Ee][Yy][Cc][Hh][Ee][Cc][Kk]>/${kkey[n]}/g")  # <SUB1REF>
	#khtml[n]="my red check box"
#   khtml[n]='<div style="border: 4px solid red; width: 10px; height: 10px; display: inline-block; margin-right: 4px;"></div>'
    khtml[n]='<div style="border: 1px solid black; width: 14px; height: 14px; display: inline-block; margin-left: 4px; margin-right: 4px;"><div style="border: 1px solid white; width: 12px; height: 12px; display: inline-block;"><div style="border: 6px solid DarkGray; display: inline-block; margin-right: 4px;"></div></div></div>'
#   khtml[n]='<div style="border: 1px solid black; width: 14px; height: 14px; display: inline-block; margin-left: 4px; margin-right: 4px;"><div style="border: 2px solid white; width: 10px; height: 10px; display: inline-block;"><div style="border: 5px solid red; display: inline-block;"></div></div></div>'
    # save key and search again
    ((n++))
    continue
  fi

  # place additional substitutions here


  # no more substitutions found, end search
  break
done

debug "SEARCH AND PROTECT FINISHED, MSG=\"$msg\""

if [ "$debugging" ] ; then
  for ((i=0;i<n;i++)) ; do
    echo "protecting pattern ${i} with ${kkey[i]}: \"${khtml[i]}\"" 1>&2
    echo "patter ${i} done" 1>&2
  done
fi

# armor remaining reserved characters (including adding <BR> to linefeeds)
msg=$(echo "$msg" | tr '\n' $'\x1E' | sed -e 's/\&/\x1F/g' -e "s/</\&lt;/g" -e "s/>/\&gt;/g" -e "s/'/\&#039;/g" -e "s/\"/\&quot;/g" -e 's/\x1F/\&amp;/g' -e 's/\x1E/\<BR\>\n/g')
debug "ARMOR FINISHED, MSG=\"$msg\""


# restore protected html
for ((i=0;i<n;i++)) ; do
  # armor key for use by SED
  k=${khtml[i]}
  debug "RESTORE \"${khtml[i]}\" from \"${kkey[i]}\""
  k=$(echo "$k" | sed "s/\&/\\\&/g" | tr '"' $'\x01' | tr '/' $'\x02')
  # replace key with html
  debug ">>  sed \"s/${kkey[i]}/$k/g\"" 
  msg=$(echo "$msg" | sed "s/${kkey[i]}/$k/g" | tr $'\x01' '"' | tr $'\x02' '/')
done

# restore linefeeds if mono blocks are present
if [ -n "$monokey" ] ; then
  msg=$(echo "$msg" | tr $'\x1E' '\n')
fi



# armor spaces at the start of lines
msg=$(echo "$msg" | sed -E 's/^ /\&nbsp;/g' | sed -E 's/\&nbsp; /\&nbsp;\&nbsp;/g')


debug "RESTORE FINISHED, MSG=\"$msg\""

# return result to caller
echo -n "$msg"

}




########################################
##  ARMOR URL
########################################

# convert a user-generated URL into a web-safe URL by armoring it for URL formatting
# (some web browsers don't mind things like spaces in URLs, but others are more strinct)

# change  %  to %25 (before all other encodes)
# change ' ' to "%20"  (often paths on web servers use "+" where a space would normally be seen)
# change  "  to %22
# change  #  to %23
# change  $  to %24
# change  &  to %26
# change  '  to %27
# change  <  to %3C
# change  >  to %3E
# change  ?  to %3F
# also converts linefeeds to <BR>'s

armorurl () {

echo "$1" | sed  -e 's/%/%25/g' -e 's/\&/\%26/g' -e "s/</%3C/g" -e "s/>/%3E/g" -e "s/'/%27/g" -e "s/\"/%22/g" -e "s/#/%23/g" -e "s/\?/%3F/g" -e "s/ /%20/g" -e 's/\$/\%24/g'

}



########################################
##  ASSERT
########################################

# check exit code of last function called, and trigger ABORT on failure

# syntax: ABORT $LINENO "description of what you were trying to do" "the CMD you executed that failed"

assert () {

rc=$?
local line try cmd
line=$1
try=$2
cmd=$3
((line--))
if [ $rc != 0 ] ; then
  echo "ASSERT failed on line $line trying to $try with CMD: $cmd" 1>&2
  exit 1
else
  debug "SUCCESS on line $line trying to $try with CMD: $cmd" 1>&2
fi

}



########################################
##  APPEND
########################################

# append a line of text to the "$new_index" file
# also appends a "<BR>" after that if caller specify "1" as a second parameter 

append () {

if [ -z "$2" ] ; then
  echo "$1" >> "$new_index"
  assert $LINENO "append line with no BR" "echo \"$1\" >> \"$new_index\""
else
  echo "$1<BR>" >> "$new_index"
  assert $LINENO "append line with BR" "echo \"$1<BR>\" >> \"$new_index\""
fi

}



########################################
##  BR
########################################

# append a "<BR>" to the "$new_index" file

BR () {

append "<br>"

}



########################################
##  CLEAR CACHE
########################################

# reset specified cache to "-"

clear_cache () {
local path key

path=$1
key=$2

write_cache "$path" "$key" "-"

}



########################################
##  DEBUG
########################################

# print colored debug message if debugging is enabled

debug () {

if [ "$switch_debug" ] ; then
  print_green "$1" 1>&2
fi

}



########################################
##  DEBUG COMMENT
########################################

# print first parameter to STDERR if comment debugging is enabled

debug_comment () {

if [ "$switch_debug_comment" ] ; then
  echo $'\e[1;94;106m'" $1 "$'\e[m' 1>&2  # bold blue text on light cyan background
fi

}



########################################
##  DEBUG CACHE
########################################

# print first parameter to STDERR if cache debugging is enabled

debug_cache () {

if [ "$switch_debug_cache" ] ; then
  echo $'\e[1;95;103m'" $1 "$'\e[m' 1>&2  # bold magenta text on dark yellow background
fi

}



########################################
##  DEBUG EXTRACT
########################################

# print first parameter to STDERR if extract debugging is enabled

debug_extract () {

if [ "$switch_debug_extract" ] ; then
  echo $'\e[1;35;43m'" $1 "$'\e[m' 1>&2  # bold magenta text on light yellow background
fi

}



########################################
##  DRYRUN
########################################

# print first parameter to STDERR

dryrun () {

echo $'\e[1;31;47m'" DRYRUN: $1 "$'\e[m' 1>&2  # bold red text on dark grey background

}





####################################
##  EXTRACT IMAGE THUMBNAIL
####################################

# extract a smaller image out of a larger image, for thumbnail viewing
# supply large image path and thumb path

extract_image_thumbnail () {
local source target source_width source_height target_width crop_width crop_height target_height

# load parameters
source=$1
target=$2

# verify parameters
if [ -z "$source" ] ; then
  abort $LINENO "no image source provided"
elif [ ! -f "$source" ] ; then
  abort $LINENO "no file found at \"$source\""
fi

# insure target file's parent foler exists
mkdir -p "${target%/*}" ; rc=$?
if [ "$rc" != 0 ] ; then
  abort $LINENO "create thumbnail parent folder" "mkdir -p \"${target%/*}\""
fi

# info message for slow operation
info "extract_image_thumbnail \"$source\" \"$target\""

# get source dimensions
get_dims "$source" $LINENO
source_width=${got_dims%,*}  # "1920"
source_height=${got_dims#*,}  # "1080"

# don't bother if target thumbnail already exists
if [ -f "$target" ] ; then
  debug "target already exists at \"$target\""
  return 0
fi

# calculate thumb width based on thumb height scaled by image's aspect ratio
((target_width=source_width*image_thumbnail_height/source_height))

# summarize
debug "image source:       \"${source}\""
if [ $got_rotated ] ; then
  debug "source dimensions:  $source_width x $source_height  (rotated)"
else
  debug "source dimensions:  $source_width x $source_height"
fi
debug "thumb target:       \"$target\""
debug "target size:        $target_width x ?"
debug "thumb target size:  $image_thumbnail_width x $image_thumbnail_height"

if [ "$target_width" -ge "$image_thumbnail_width" ] ; then
  debug "image is square or landscape, just scale it down"

  # summarize
  debug "thumb dimensions:   $target_width x $image_thumbnail_height"

  # extract
  if [ "$switch_dryrun" ] ; then
    dryrun "would extract thumb from imge with CMD: ffmpeg -i \"${source}\" -vf scale=${target_width}:${image_thumbnail_height} \"${target}\""
  else
    debug_extract "extract CMD: ffmpeg -i \"${source}\" -vf scale=${target_width}:${image_thumbnail_height} \"${target}\""
    ffmpeg -i "${source}" -vf scale=${target_width}:${image_thumbnail_height} "${target}" 2> /dev/null > /dev/null ; rc=$?
    if [ $rc == 0 ] ; then
      debug "EXTRACT COMPLETE"
    else
      abort $LINENO "ffmpeg returned code $rc"
    fi
  fi
else
  debug "image is in portrait (taller than wide), crop verically to correct aspect ratio before scaling down"

  # scale up target_width
  ((target_height=image_thumbnail_height))
  ((target_width=image_thumbnail_width*target_height/image_thumbnail_height))

  ((crop_width=source_width))
  ((crop_height=crop_width*image_thumbnail_height/image_thumbnail_width))
  ((crop_x=0))
  ((crop_y=(source_height-crop_height)/2))

  # summarize
  debug "thumb dimensions:   $target_width x $image_thumbnail_height"
  debug "source crop size:   $crop_width x $crop_height"
  debug "source crop at:     $crop_x x $crop_y"

  # extract
  if [ "$switch_dryrun" ] ; then
    dryrun "would extract thumb from image with CMD: ffmpeg -i \"${source}\" -vf crop=$crop_width:$crop_height:$crop_x:$crop_y,scale=$target_width:$target_height \"${target}\""
  else
    debug_extract "extract CMD: ffmpeg -i \"${source}\" -vf crop=$crop_width:$crop_height:$crop_x:$crop_y,scale=$target_width:$target_height \"${target}\""
    ffmpeg -i "${source}" -vf crop=$crop_width:$crop_height:$crop_x:$crop_y,scale=$target_width:$target_height "${target}" 2> /dev/null > /dev/null ; rc=$?
    if [ $rc == 0 ] ; then
      debug "EXTRACT COMPLETE"
    else
      abort $LINENO "ffmpeg returned code $rc trying CMD: ffmpeg -i \"${source}\" -vf crop=$crop_width:$crop_height:$crop_x:$crop_y,scale=$target_width:$target_height \"${target}\""
    fi
    # cache new thumbnail size, assume create was successful and it's not rotated
    write_cache "${target}" "dims" "${target_width},${target_height}"
    write_cache "${target}" "rotated" ""
  fi
fi

}



########################################
##  GET COMMENT
########################################

# loads got_comment with Finder comment for specified file path
# uses/updates cache


got_comment=
get_comment () {
local path name

path=$1

name="${path##*/}"

if ! [ -e "$path" ] ; then
  debug "file/folder not found at \"$path\""
  got_comment=
  return
fi

# use cache if available
if read_cache "$path" "comment" ; then
  # comment is cached
  got_comment=$read_value
  return
fi

# comment not cached
got_comment=

# info message for slow operation
#info "got_comment \"$path]\""

got_comment=$(xattr -p "com.apple.metadata:kMDItemFinderComment" "$path" 2> /dev/null) ; rc=$?  # ignore errors since xattr will exit with code 1 if no comments are found
#debug_comment "COMMENT CHECK: xattr -p \"com.apple.metadata:kMDItemFinderComment\" \"$path\"  -  got_comment=\"$got_comment\", rc=$rc"

if [ ${#got_comment} == 0 ] ; then
  # no comments
  test
elif [ ${#got_comment} -le 25 ] ; then
  # short comment (under 16 characters)
  #echo -n "SHORT: "
  got_comment="${got_comment:9:${#got_comment}-10}"
else
  # -ge 28 = longer comment (between 16 and 255 characters
  #echo -n "LONG: "
  got_comment="${got_comment:11:${#got_comment}-12}"
fi

if [ -n "$got_comment" ] ; then
  debug_comment "found comment on \"$path\":  \"$got_comment\""
fi

# update cache
write_cache "$path" "comment" "$got_comment"

}



####################################
##  EXTRACT VIDEO THUMBNAIL
####################################

# extract a still image out of a video, for thumbnail previewing
# supply video path and thumb path

extract_video_thumbnail () {
local source target sec time_index source_width source_height target_width crop_width crop_height target_height

# load parameters
source=$1
target=$2

# verify parameters
if [ -z "$source" ] ; then
  abort $LINENO "no video source provided"
elif [ ! -f "$source" ] ; then
  abort $LINENO "no file found at \"$source\""
fi

# insure target file's parent foler exists
mkdir -p "${target%/*}" ; rc=$?
if [ "$rc" != 0 ] ; then
  abort $LINENO "create thumbnail parent folder" "mkdir -p \"${target%/*}\""
fi

# info message for slow operation
info "extract_video_thumbnail \"$source\" \"$target\""

# get video length in seconds and stamp
sec=$(avmediainfo "$source" --metadata | grep seconds$ | head -n 1)  # "Duration: 61.018 seconds (61018/1000)"
sec=${sec% seconds*}  # "Duration: 61.018"
sec=${sec%.*}  # "Duration: 61"
sec=${sec#* }  # "61"
#if [[ (-z "$sec") || ($sec == 0) ]] ; then
if [ -z "$sec" ] ; then
  abort $LINENO "unable to extract video duration metadata from \"$source\""
fi
sec_stamp=$(date -j -u -f "%s" "$sec" "+%H:%M:%S")  # "01:23:45"

# get source dimensions
get_dims "$source" $LINENO
source_width=${got_dims%,*}  # "1920"
source_height=${got_dims#*,}  # "1080"

# don't bother if target thumbnail already exists
if [ -f "$target" ] ; then
  debug "target already exists at \"$target\""
  return 0
fi

# calculate time index to extract from


#goto

#switch_debug=1
#switch_debug_cache=1

if read_cache "$path" "offset" ; then
  debug "thumbnail time offset was cached from comment, \"$read_value\""
  time_index=$read_value
else
  time_index=
  get_comment "$path"
  debug "comment = \"$got_comment\""
  local v tmin tsec
  if [ ${#got_comment} == 5 ] ; then
    debug "length is good for 00:00"
    v=${got_comment%%:*}
    if [ ${#v} != ${#got_comment} ] ; then
      if [ "${tmin:0:1}" == "0" ] ; then
        tmin=${tmin:1}
      fi
      tmin=$((v))
    fi
    v=${got_comment##*:}
    if [ ${#v} != ${#got_comment} ] ; then
      if [ "${tsec:0:1}" == "0" ] ; then
        tsec=${tsec:1}
      fi
      tsec=$((v))
    fi

    debug "tmin = \"$tmin\", tsec = \"$tsec\""
    if [[ (-n "$tmin") && (-n "$tsec") && ("$tsec" -lt 60) ]] ; then
      # probably a valid time index
      ((time_index=tmin*60+tsec))
      debug "time_index = \"$time_index\""
      write_cache "$path" "offset" "$time_index"
    fi
  fi
fi

if [ -z "$time_index" ] ; then
  # time index not specified in comment, use default
  if [ "$sec" -lt $((default_video_index*2)) ] ; then
    # videos less than twice the default get sampled from the middle
    ((time_index=sec/2))
  else
    time_index=$default_video_index
  fi
  time_index=$(date -j -u -f "%s" "$time_index" "+%H:%M:%S")
fi

# summarize
debug
debug "video source:       \"${source}\""
debug "source dimensions:  $source_width x $source_height"
debug "source length:      $sec_stamp  ($sec seconds)"
debug "extract at sec:     $time_index"
debug "thumb target:       \"$target\""

# calculate thumb width based on thumb height scaled by video's aspect ratio
((target_width=source_width*video_thumbnail_height/source_height))

if [ "$target_width" -ge "$video_thumbnail_height" ] ; then
  # video is square or landscape, just scale it down

  # summarize
  debug "thumb dimensions:   $target_width x $video_thumbnail_height"

  # extract
  if [ "$switch_dryrun" ] ; then
    dryrun "would extract thumb from video with CMD: ffmpeg -ss ${time_index} -i \"${source}\" -frames:v 1 -q:v 2 -vf scale=${target_width}:${video_thumbnail_height} \"${target}\""
  else
    debug_extract "extract CMD: ffmpeg -ss ${time_index} -i \"${source}\" -frames:v 1 -q:v 2  -vf scale=${target_width}:${video_thumbnail_height} \"${target}\""
    ffmpeg -ss ${time_index} -i "${source}" -frames:v 1 -q:v 2 -vf scale=${target_width}:${video_thumbnail_height} "${target}" 2> /dev/null > /dev/null ; rc=$?
    if [ $rc == 0 ] ; then
      debug "EXTRACT COMPLETE"
    else
      abort $LINENO "ffmpeg returned code $rc"
    fi
    # cache new thumbnail size, assume create was successful and it's not rotated
switch_debug_cache=1
    write_cache "${target}" "dims" "${target_width},${video_thumbnail_height}"
    write_cache "${target}" "rotated" ""
switch_debug_cache=
  fi
else
  # video is in portrait (taller than wide), crop verically to bring aspect ratio to 5:3 before scaling down

  # scale up target_width to make thumb ratio 3:2 (16:9 or even 5:3 would be too wide)
  ((target_height=video_thumbnail_height))
  ((target_width=video_thumbnail_width*target_height/video_thumbnail_height))

  ((crop_width=source_width))
  ((crop_height=crop_width*video_thumbnail_height/video_thumbnail_width))
  ((crop_x=0))
  ((crop_y=(source_height-crop_height)/2))

  # summarize
  debug "thumb dimensions:   $target_width x $video_thumbnail_height"
  debug "source crop size:   $crop_width x $crop_height"
  debug "source crop at:     $crop_x x $crop_y"

  # extract
  if [ "$switch_dryrun" ] ; then
    dryrun "would extract thumb from video with CMD: ffmpeg -ss ${time_index} -i \"${source}\" -frames:v 1 -q:v 2 -vf crop=$crop_width:$crop_height:$crop_x:$crop_y,scale=$target_width:$target_height \"${target}\""
  else
    debug_extract "extract CMD: ffmpeg -ss ${time_index} -i \"${source}\" -frames:v 1 -q:v 2 -vf crop=$crop_width:$crop_height:$crop_x:$crop_y,scale=$target_width:$target_height \"${target}\""
    ffmpeg -ss ${time_index} -i "${source}" -frames:v 1 -q:v 2 -vf crop=$crop_width:$crop_height:$crop_x:$crop_y,scale=$target_width:$target_height "${target}" 2> /dev/null > /dev/null ; rc=$?
    if [ $rc == 0 ] ; then
      debug "EXTRACT COMPLETE"
    else
      abort $LINENO "ffmpeg returned code $rc"
    fi
    # cache new thumbnail size, assume create was successful and it's not rotated
switch_debug_cache=1
    write_cache "${target}" "dims" "${target_width},${target_height}"
    write_cache "${target}" "rotated" ""
switch_debug_cache=
  fi
fi

}



########################################
##  GET DIMS
########################################

# loads got_dims with dimensions of specified image or video file path (in "w,h" format)
# also loads got_rotated with blank or "rotated", and if rotated, got_dims will contain DISPLAYED w,h (which will be h,w), which is what ffmpeg requires
# uses/updates cache

got_dims=
got_rotated=

get_dims () {

local path afile name called_from w h might_be_swapped fftemp

path=$1
called_from=$2

debug "GET_DIMS called from \"$called_from\" for path \"$path\""

name="${path##*/}"

# verify file/folder exists
if ! [ -e "$path" ] ; then
  abort $LINENO "file/folder not found at \"$path\", called from \"$called_from\", CWD = \"$(pwd)\""
  #debug "file/folder not found at \"$path\""
  #got_dims=
  #got_rotated=
  #return
fi

# use cache if available
if read_cache "$path" "dims" ; then
  # dims is cached
  got_dims=$read_value
  if read_cache "$path" "rotated" ; then
    # if dims are cached, rotation should also be cached
    got_rotated=$read_value
    if [ $got_rotated ] ; then
      # swap h,v for rotated image/video
      w=${got_dims%,*}
      h=${got_dims#*,}
      got_dims="${h},${w}"
    fi
    debug "returning cached got_dims \"$got_dims\""
    return
  fi
  # dims are cached but rotation is not, re-fetch both
  clear_cache "$path" "dims"
  clear_cache "$path" "rotated"
fi

# info message for slow operation
#info "get_dims \"$path]\" \"$called_from\""

# dims not cached
got_dims=
got_rotated=
might_be_swapped=

# try to get dimensions using avmediainfo (probably won't work with images, only does videos?)
# avmediainfo returns dimensions adjusted for orientation
got_dims=$(avmediainfo "$path" --metadata | grep Dimensions | head -n 1) ; rc=$? # "Dimensions: 1080 x 1920"  (that example is a rotated dim)
debug "avmediainfo returned \"$got_dims\", rc=$rc"
if [ -n "$got_dims" ] ; then
  got_dims=${got_dims#*: }  # "1080 x 1920"
  w=${got_dims%% *}  # "1080"
  h=${got_dims##* }  # "1920"
  got_dims="${w},${h}"  # "1080,1920"
  # avmediainfo swaps dims if rotated, so we might need to swap them back before caching
  might_be_swapped=1
fi

# if that didn't work, try to get full picture's dimensions via ffmpeg  (it will return native dimensions, IGNORING ROTATION)
# ffmpeg returns dimensions assuming (encoded) orientation 0
if [ -z "$got_dims" ] ; then
  got_dims=$(ffmpeg -i "$path" 2>&1 | grep Video: | grep -o " [0-9]\{2,5\}x[0-9]\{2,5\}[, ]" | cut -d ' ' -f 2 | cut -d ',' -f 1 | tr x ,) ; rc=$?  # 1024,768
  debug "ffmpeg returned \"$got_dims\""
  if [ -z "$got_dims" ] ; then
    debug "got rc=$rc trying to get dimensions with CMD: ffmpeg -i \"$path\" 2>&1 | grep Video: | grep -o \" [0-9]\{2,5\}x[0-9]\{2,5\}[, ]\" | cut -d ' ' -f 2 | cut -d ',' -f 1 | tr x ,)" 1>&2
  fi
fi

# if that didn't work, try applescrpting Image Events
# Image Events returns dimensions assuming (encoded) orientation 0
if [ -z "$got_dims" ] ; then
  afile=$(armor_for_applescript "$path")
  got_dims=$(osascript -e "set unix_path to \"$afile\"" -e "tell application \"Image Events\"" -e "set img to open unix_path" -e "set props to the properties of img" -e "close img" -e "set width to item 1 of dimensions of props" -e "set height to item 2 of dimensions of props" -e "end tell" -e "copy width&\",\"&height as string to stdout") ; rc=$?  # "1024,768"
  debug "got rc=$rc trying to get full picture's dimensions with CMD: osascript -e \"set unix_path to \\\"$afile\\\"\" -e \"tell application \\\"Image Events\\\"\" -e \"set img to open unix_path\" -e \"set props to the properties of img\" -e \"close img\" -e \"set width to item 1 of dimensions of props\" -e \"set height to item 2 of dimensions of props\" -e \"end tell\" -e \"copy width&\\\",\\\"&height as string to stdout\""
  if [ -z "$got_dims" ] ; then
    echo "failed to get dims and got RC=$rc with CMD: osascript -e \"set unix_path to \\\"$afile\\\"\" -e \"tell application \\\"Image Events\\\"\" -e \"set img to open unix_path\" -e \"set props to the properties of img\" -e \"close img\" -e \"set width to item 1 of dimensions of props\" -e \"set height to item 2 of dimensions of props\" -e \"end tell\" -e \"copy width&\\\",\\\"&height as string to stdout\"" 1>&2
  fi
  debug "Image Events returned \"$got_dims\""
fi

# abort if failed to get dimensions
if [ -z "$got_dims" ] ; then
  abort $LINENO "failed to get dimensions (by any method) of \"$path\", called from line $called_from"
fi

# ffmpeg requires a temp file with the correct file extension
fftemp="${temp_file}.${path##*.}"
remove "$fftemp" 50

# test for rotation
w=${got_dims%,*}  # 1080
h=${got_dims#*,}  # 1920
debug "testing for rotation with CMD: ffmpeg -i \"${path}\" -frames:v 1 -vf crop=$w:$h \"${fftemp}\""
ffmpeg -i "${path}" -frames:v 1 -vf crop=$w:$h "${fftemp}" 2>/dev/null ; rc=$?
if [ "$rc" == 0 ] ; then
  debug "ffmpeg crop succeeeded, not rotated"
else
  # ffmpeg failed to crop, indicating w/h have been swapped - display orientation is either 90 or 270 degrees from encoded orientation
  debug "ffmpeg crop failed, image is rotated 90 or 270 degrees"
  got_rotated="rotated"
  # swap dims back to encoded orientation if pre-swapped (probably by avmediainfo if rotated)
  if [ $might_be_swapped ] ; then
    debug "dims were swapped, swapping them back"
    got_dims="${h},${w}"  # "1920,1080"
  else
    debug "dims were not swapped, leaving them as-is"
  fi
fi

# remove temp file if it exists
remove "${fftemp}" 50  # MAX images can be quite large

# update cache
write_cache "$path" "dims" "$got_dims"  # stores dimensions of encoded orientation
write_cache "$path" "rotated" "$got_rotated"

# got_dims will return DISPLAY dimensions, so swap w/h if rotated
if [ $got_rotated ] ; then
  # swap h,v for rotated image/video
  w=${got_dims%,*}
  h=${got_dims#*,}
  got_dims="${h},${w}"
fi

}

get_dims_standardized () {

local path called_from
local scale iw ih tw th pw path2 key f2

path=$1
called_from=$2

get_dims "$path" "$called_from"
iw=${got_dims%,*}
ih=${got_dims#*,}
((scale=iw*100/ih))
if [[ ($scale -ge 145) && ($scale -le 155) ]] ; then
  # close enough
  return
fi

echo "MAX image \"$path\" needs proportions adjusted:"

if [[ $scale -lt 150 ]] ; then
  echo "  scale $scale needs left/right padding"
  ((th=ih))
  ((tw=th*3/2))
else
  echo "  scale $scale needs top/bottom padding"
  ((tw=iw))
  ((th=tw*2/3))
fi
((pw=(tw-iw)/2))
((ph=(th-ih)/2))
echo "  scale from ($iw,$ih) to ($tw,$th), padding ($pw,$ph)"

path2="${path%/*}/resized_${path##*/}"
echo "path2 = \"$path2\""
#read -r -p "verify pathname and press any key : " -n1 key

# iw+pw+pw can't be less than tw
# ih+ph+ph can't be less than th
# they can be low by 1 if there's a round in the /2 above  "Padded dimensions cannot be smaller than input dimensions."
# if (scale + pad + pad) > target, adjust target up to match scale+padding
((tw2=iw+pw+pw))
((th2=ih+ph+ph))

# also, another bug requires the target to be even if any padding is performed or you'll also get a smaller error
if [ $((tw2/2*2)) != $tw2 ] ; then
  ((tw2++))
fi
if [ $((th2/2*2)) != $th2 ] ; then
  ((th2++))
fi

if [ $tw -lt $tw2 ] ; then
  echo "increasing target width from $tw to $tw2"
  ((tw=tw2))
fi
if [ $th -lt $th2 ] ; then
  echo "increasing target width from $tw to $tw2"
  ((th=th2))
fi



echo "CMD: ffmpeg -y -i \"$path\" -vf \"scale=$iw:$ih,pad=$tw:$th:$pw:$ph:color=white\" \"$path2\""
ffmpeg -y -i "$path" -vf "scale=$iw:$ih,pad=$tw:$th:$pw:$ph:color=white" "$path2" ; rc=$?
if [ "$rc" != 0 ] ; then
  abort $LINENO "got rc=$rc" "ffmpeg -y -i \"$path\" -vf \"scale=$iw:$ih,pad=$tw:$th:$pw:$ph:color=white\" \"$path2\""
fi
#read -r -p "verify image at \"$path2\" and press any key : " -n1 key

if [ -f "$path2" ] ; then
  rm "$path"
  mv "$path2" "$path"
fi

clear_cache "$path" "dims"
clear_cache "$path" "rotated"

f2="${path%/*}/../full/${path##*/}"
if [ -f "$f2" ] ; then
  #read -r -p "removing \"$f2\" and press any key : " -n1 key
  rm "$f2"
fi
f2="${path%/*}/../thumb/${path##*/}"
if [ -f "$f2" ] ; then
  #read -r -p "removing \"$f2\" and press any key : " -n1 key
  rm "$f2"
fi

get_dims "$path" "$called_from"

}




########################################
##  GET SEC
########################################

# loads got_sec and got_stamp with duration of specified video file path
# uses/updates cache

got_sec=
got_stamp=

get_sec () {
local path rc

path=$1

# verify file exists
if ! [ -f "$path" ] ; then
  debug "file not found at \"$path\""
  got_sec=
  got_stamp=
  return
fi

# use cache if available
if read_cache "$path" "sec" ; then
  # sec is cached
  got_sec=${cache_sec[cache_index]}
  got_stamp=${cache_stamp[cache_index]}  # if cache_sec is available, cache_stamp should also be available
  return
fi

# info message for slow operation
#info "get_sec \"$path]\""

# sec not cached
got_sec=
got_stamp=

# use avmediainfo to get video length
got_sec=$(avmediainfo "$path" --metadata | grep seconds | head -n 1)  # "Duration: 61.018 seconds (61018/1000)"
got_=${got_% seconds*}  # "Duration: 61.018"
got_=${got_%.*}  # "Duration: 61"
got_=${got_#* }  # "61"
if [[ (-z "$sec") || ($sec == 0) ]] ; then
  abort $LINENO "failed to get video length with CMD: avmediainfo \"$path\" --metadata | grep seconds | head -n 1"
fi

# generate timestamp
got_stamp=$(date -j -u -f "%s" "$got_sec" "+%H:%M:%S") ; rc=$?  # "01:23:45"
if [ -z "$got_stamp" ] ; then
  abort $LINENO "failed to create stamp with CMD: date -j -u -f \"%s\" \"$got_sec\" \"+%H:%M:%S\""
elif [ "$rc" != 0 ] ; then
  abort $LINENO "create stamp with CMD: date -j -u -f \"%s\" \"$got_sec\" \"+%H:%M:%S\" (got rc=$rc)"
fi

# update cache
write_cache "$path" "sec" "$got_sec"
write_cache "$path" "stamp" "$got_stamp"

}



########################################
##  GET STAMP
########################################

# loads got_sec and got_stamp with duration of specified video file path
# uses/updates cache

get_stamp () {
local path

path=$1

# verify file exists
if ! [ -f "$path" ] ; then
  debug "file not found at \"$path\""
  got_stamp=
  return
fi

# use cache if available
if read_cache "$path" "stamp" ; then
  # stamp is cached
  got_stamp${cache_sec[cache_index]}
  got_stamp=${cache_stamp[cache_index]}  # if cache_sec is available, cache_stamp should also be available
  return
fi

# info message for slow operation
#info "get_stamp \"$path]\""

# sec not cached
get_sec "$path"  # will take care of setting stamp and caching sec/stamp

}





########################################
##  GET URL
########################################

# loads got_url with .webloc url for specified file path
# uses/updates cache

got_url=

get_url () {
local path name pl

path=$1

name="${path##*/}"

# verify file/folder exists
if ! [ -e "$path" ] ; then
  debug "file/folder not found at \"$path\""
  got_url=
  return
fi

# use cache if available
if read_cache "$path" "url" ; then
  # url is cached
  got_url=${cache_url[cache_index]}
  return
fi

# info message for slow operation
#info "get_url \"$path]\""

# url not cached
got_url=

# defaults requires file to end in .plist, so make a copy for it to use
pl="${path%/*}/${plist}"
cp "$path" "$pl"
assert $LINENO "copy webloc to temp plist" "cp \"$path\" \"$pl\""
if ! [ -r "$pl" ] ; then
  echo "cp \"$path\" \"$pl\""
  abort $LINENO "copy succeeded but file does not exist at \"$pl\""
fi

# use defaults to extract url from .webloc file
got_url=$(defaults read "$pl" "URL")
assert $LINENO "read URL from web browser bookmark" "defaults read \"$pl\" \"URL\" (webloc file at \"${path}\""

# remove temp plist
remove "$pl" 1

# update cache
write_cache "$path" "url" "$got_url"

}



########################################
##  INFO
########################################

# print colored info message to STDERR

info () {

print_yellow "$1" 1>&2

}



########################################
##  LOAD CACHE
########################################

# load cache data from this folder's cache file

cache_modified=
caches=0
cache_selected=""
cache_index=-1
cache_version=5

load_cache () {
local x key value line file_vers

# it's ok if cache file doesn't exist
if [ ! -f "$cache_file" ] ; then
  debug_cache "no cache file found at \"${root}/${cache_file}\""
  return
elif [ "$switch_rebuild_caches" ] ; then
  if [ "$switch_dryrun" ] ; then
    dryrun "would have removed cache file at \"${root}/${cache_file}\""
  else
    info "removing cache file at \"${root}/${cache_file}\""
    remove "$cache_file" 1
  fi
  return
fi

line=0
debug_cache "loading cache file at \"${root}/${cache_file}\""
IFS2=$FS
IFS=$'\n'
while read -r cache_path[caches] ; do
  ((line++))
  if [ -z "${cache_path[caches]}" ] ; then
    abort $LINENO "unexpected end of cache file \"$(pwd)/${cache_file}\" on line $line reading name"
  fi
  if [ -z "$file_vers" ] ; then
    # the first line of the cache file is the cache file's version number
    file_vers="${cache_path[0]}"
    debug_cache "cache file version = \"$file_vers\""
    if [ "$file_vers" != "$cache_version" ] ; then
      cache_modified=1
    fi
    continue
  fi
  # subsequent lines are (unmarked) groups of lines that make up cache file entries
  
  if [ "$file_vers" == 1 ] ; then
    read -r cache_size[caches]
    read -r cache_dims[caches]
    read -r cache_url[caches]
    read -r cache_comment[caches]
    cache_sec[caches]="-"
    cache_stamp[caches]="-"
    if [ "${cache_dims[caches]}" != "-" ] ; then
      debug "clearning DIMS cache for \"${cache_path[caches]}\""
      cache_dims[caches]="-"
    fi
    cache_rotated[caches]="-"
  elif [ "$file_vers" == 2 ] ; then
    read -r cache_size[caches]
    read -r cache_dims[caches]
    read -r cache_url[caches]
    read -r cache_sec[caches]
    read -r cache_stamp[caches]
    read -r cache_comment[caches]
    if [ "${cache_dims[caches]}" != "-" ] ; then
      debug "clearning DIMS cache for \"${cache_path[caches]}\""
      cache_dims[caches]="-"
    fi
    cache_rotated[caches]="-"
  elif [ "$file_vers" == 3 ] ; then
    read -r cache_size[caches]
    read -r cache_dims[caches]
    read -r cache_url[caches]
    read -r cache_sec[caches]
    read -r cache_stamp[caches]
    read -r cache_comment[caches]
    if [ "${cache_dims[caches]}" != "-" ] ; then
      debug "clearning DIMS cache for \"${cache_path[caches]}\""
      cache_dims[caches]="-"
    fi
    cache_rotated[caches]="-"
  elif [ "$file_vers" == 4 ] ; then
    read -r cache_size[caches]
    read -r cache_dims[caches]
    read -r cache_rotated[caches]
    read -r cache_url[caches]
    read -r cache_sec[caches]
    read -r cache_stamp[caches]
    read -r cache_comment[caches]
  elif [ "$file_vers" == 5 ] ; then
    read -r cache_size[caches]
    read -r cache_dims[caches]
    read -r cache_rotated[caches]
    read -r cache_url[caches]
    read -r cache_sec[caches]
    read -r cache_stamp[caches]
    read -r cache_offset[caches]
    read -r cache_comment[caches]
  else
    abort $LINENO "encountered unsupported file cache version \"$file_vers\" in cache file at \"$cache_file\""
  fi
  # entries are terminated by "==="
  read -r x

 # localize cache paths

  if [ "${cache_path[caches]:0:1}" == "/" ] ; then
    # cache is absolute (needs to be made relative)
    if [ -e "${cache_path[caches]}" ] ; then
      # may be a thumb for an alias, ignore it
      debug_cache "good absolute cache"
      #  don't continue
    elif [ "${cache_path[caches]:0:${#root}}" != "$root" ] ; then
      # path does not match current root, toss it
      info "purging orphaned absolute cache1 \"${cache_path[caches]}\""
      cache_modified=1
      continue
    elif [ ! -e "${cache_path[caches]}" ] ; then
      # path maps to this folder, but object is missing, toss it
      info "purging orphaned absolute cache2 \"${cache_path[caches]}\""
      cache_modified=1
      continue
    else
      # path maps to this folder and object exists, localize it
      info "localizing path \"${cache_path[caches]}\" to \"${cache_path[caches]:${#root}+1}\""
      cache_path[caches]=${cache_path[caches]:${#root}+1}
      cache_modified=1
    fi
  elif [ ! -e "${cache_path[caches]}" ] ; then
    # path is relative but object is missing, toss it
    info "purging orphaned relative cache \"${cache_path[caches]}\""
    cache_modified=1
    continue

  else
    # path is relative and object exists, load it
    debug_cache "good relative cache"
  fi

  debug_cache "cache entry ${i}:"
  debug_cache "   path=\"${cache_path[caches]}\""     # full unix path to file or folder
  debug_cache "   size=\"${cache_size[caches]}\""     # size in bytes (files only)
  debug_cache "   dims=\"${cache_dims[caches]}\""     # image or video file dimensions, unrotated - encoded dimensions before display rotation - format: "w,h"
  debug_cache "rotated=\"${cache_rotated[caches]}\""  # image or video display rotation - rotated means 90 or 270 degrees, blank means 0 or 180 degrees - format: "rotated"|""
  debug_cache "    url=\"${cache_url[caches]}\""      # url of webloc file
  debug_cache "    sec=\"${cache_sec[caches]}\""      # duration of video file - format "sec"
  debug_cache "  stamp=\"${cache_stamp[caches]}\""    # duration of video file - format "hh:mm:ss"
  debug_cache "comment=\"${cache_comment[caches]}\""  # Finder comment
  ((caches++))
done < "$cache_file"  # there is no end-of-file marker or entry count, just read entries until you hit EOF
IFS=$IFS2
debug_cache "loaded $caches cache entries"

}



########################################
##  PRINT BLUE
########################################

# print provided text in bold blue text on bright cyan background

print_blue () {

echo $'\e[1;94;106m'" $1 "$'\e[m'

}



########################################
##  PRINT GREEN
########################################

# print provided text in bold blue text on bright green background

print_green () {

echo $'\e[1;94;102m'" $1 "$'\e[m'

}



########################################
##  PRINT RED
########################################

# print provided text in bold yellow text on bright red background

print_red () {

echo $'\e[1;93;101m'" $1 "$'\e[m'

}



########################################
##  PRINT YELLOW
########################################

# print provided text in bold magenta text on bright yellow background

print_yellow () {

echo $'\e[1;95;103m'" $1 "$'\e[m'

}



########################################
##  READ CACHE
########################################

# read cache entry by full file path and key (creates cache if necessarty)
# stores value in read_value global
# also returns true if found, or false if null

read_cache () {
local path key

path=$1
key=$2

# select cache (creates if necessary)
select_cache "$path"

# read specified cache value, store in read_value global
if [ "$key" == "size" ] ; then
  read_value=${cache_size[cache_index]}
elif [ "$key" == "dims" ] ; then
  read_value=${cache_dims[cache_index]}
elif [ "$key" == "rotated" ] ; then
  read_value=${cache_rotated[cache_index]}
elif [ "$key" == "url" ] ; then
  read_value=${cache_url[cache_index]}
elif [ "$key" == "sec" ] ; then
  read_value=${cache_sec[cache_index]}
elif [ "$key" == "stamp" ] ; then
  read_value=${cache_stamp[cache_index]}
elif [ "$key" == "offset" ] ; then
  read_value=${cache_offset[cache_index]}
elif [ "$key" == "comment" ] ; then
  read_value=${cache_comment[cache_index]}
else
  abort $LINENO "read_cache given unsupported key \"$key\""
fi
debug_cache "read cached key $key from \"$path\" = \"$read_value\""

# return exit code
if [ "$read_value" == "-" ] ; then
  print_blue "cache miss for key \"$key\" at path \"$path\""
  return 1
else
  return 0
fi

}



########################################
##  REMOVE
########################################

# remove a file or folder (recursive)
# also deletes cache for that object if found
# you must also supply the max allowable file/folder size in MB

remove () {
local path mb limit

path=$1
limit=$((${2:-}))


# insure size was supplied

if [ -z "$limit" ] ; then
  abort $LINENO "REMOVE called on object \"$path\" without supplying size limit"
fi


# return if object not found
if ! [ -e "$path" ] ; then
  debug "REMOVE did not find object at \"$path\""
  return
fi

# insure object is at or below size limit

mb=$(du -sm "$path" | cut -f 1)  # 266
debug "REMOVE called on \"$path\" with a limit of ${limit} MB"
if [ "$mb" -gt "$limit" ] ; then
  abort $LINENO "REMOVE called on object \"$path\" (size ${mb} MB) with a limit of only ${limit} MB"
fi


# remove object

if [ $switch_dryrun ] ; then
  dryrun "would remove object with CMD: rm -Rf \"$path\""
else
  debug "remove Finder object \"$path\""
  rm -Rf "$path"
  assert $LINENO "remove Finder object" "rm -Rf \"$path\""
fi


# remove cache for this path if it exists

if test_cache "$path" ; then
  # object was cached
  uncache "$path"
else
  # object was not cached
  debug "no cache to remove for \"$path\""
fi

}



########################################
##  SAVE CACHE
########################################

# if cache has been modified, write cache to cache file in current folder

save_cache () {
local i

if ! [ "$cache_modified" ] ; then
  debug_cache "not saving cache, no changes"
  # no changes made, no need to save file
  return
fi

# dryrun option
if [ "$switch_dryrun" ] ; then
  dryrun "would have saved new cache file"
  cache_modified=
  return
fi

# create cache file and write cache file entries
debug_cache "saving cache file at \"${root}/${cache_file}\""
echo "$cache_version" > "$cache_file"
for ((i=0;i<caches;i++)) ; do
  echo "${cache_path[i]}"    >> "$cache_file"
  echo "${cache_size[i]}"    >> "$cache_file"
  echo "${cache_dims[i]}"    >> "$cache_file"
  echo "${cache_rotated[i]}" >> "$cache_file"
  echo "${cache_url[i]}"     >> "$cache_file"
  echo "${cache_sec[i]}"     >> "$cache_file"
  echo "${cache_stamp[i]}"   >> "$cache_file"
  echo "${cache_offset[i]}"  >> "$cache_file"
  echo "${cache_comment[i]}" >> "$cache_file"
  echo "===" >> "$cache_file"
  debug_cache "cache entry ${i}:"
  debug_cache "   path=\"${cache_path[i]}\""
  debug_cache "   size=\"${cache_size[i]}\""
  debug_cache "   dims=\"${cache_dims[i]}\""
  debug_cache "rotated=\"${cache_rotated[i]}\""
  debug_cache "    url=\"${cache_url[i]}\""
  debug_cache "    sec=\"${cache_sec[i]}\""
  debug_cache "  stamp=\"${cache_stamp[i]}\""
  debug_cache " offset=\"${cache_offset[i]}\""
  debug_cache "comment=\"${cache_comment[i]}\""
done
debug_cache "saved $caches cache entries"

# once saved, it's no longer modified
cache_modified=

}



########################################
##  SELECT CACHE
########################################

# select cache entry by file path.  creates new cache entry (with null values) if not found

select_cache () {
local path i

path=$1

# insure path is relative
if [ "${path:0:${#root}}" == "$root" ] ; then
  debug_cache "localizing path \"$path\" to \"${path:${#root}+1}\""
  path=${path:${#root}+1}
fi

# if this cache entry is alredy selected, just return
if [ "$cache_selected" == "$path" ] ; then
  debug_cache "cache selection remains unchanged at \"$cache_selected\" ($cache_index)"
  return
fi

# search for cache entry by path
for ((i=0;i<caches;i++)) ; do
  if [ "${cache_path[i]}" == "$path" ] ; then
    # cache entry found, set path and index
    cache_selected="$path"
    cache_index="$i"
    debug_cache "cache selection changed to \"$cache_selected\" ($cache_index)"
    return
  fi
done

# cache entry not found, create new entry (with NULL values) and select
cache_path[caches]="$path"
cache_size[caches]="-"
cache_dims[caches]="-"
cache_rotated[caches]="-"
cache_url[caches]="-"
cache_sec[caches]="-"
cache_stamp[caches]="-"
cache_offset[caches]="-"
cache_comment[caches]="-"
cache_selected="$path"
cache_index="$caches"
((caches++))
debug_cache "creating new cache for path \"$cache_selected\" (at index $cache_index)"

# also mark cache as modified
cache_modified=1

}



########################################
##  TEST CACHE
########################################

# test cache for key - returns TRUE if found

test_cache () {
local path i

path=$1

# insure path is relative
if [ "${path:0:${#root}}" == "$root" ] ; then
  debug_cache "localizing path \"$path\" to \"${path:${#root}+1}\""
  path=${path:${#root}+1}
fi

# search for cache entry by path
for ((i=0;i<caches;i++)) ; do
  if [ "${cache_path[i]}" == "$path" ] ; then
    # cache entry found
    return 0
  fi
done

# not found
return 1

}



########################################
##  UNCACHE
########################################

# delete specified cache - make sure it exists because this will abort if not found

uncache () {
local path i

path=$1

# insure path is relative
if [ "${path:0:${#root}}" == "$root" ] ; then
  debug_cache "localizing path \"$path\" to \"${path:${#root}+1}\""
  path=${path:${#root}+1}
fi

# search for cache entry by path
for ((i=0;i<caches;i++)) ; do
  if [ "${cache_path[i]}" == "$path" ] ; then
    # cache entry found
    if [ "$caches" == 1 ] ; then
      # uncaching only entry
      caches=0
    else
      # decrement cache entry count and overwrite this cache entry with the last cache entry
      info "removed from cache \"$path\""
      ((caches--))
      cache_path[i]=${cache_path[caches]}
      cache_size[i]=${cache_size[caches]}
      cache_dims[i]=${cache_dims[caches]}
      cache_rotated[i]=${cache_rotated[caches]}
      cache_url[i]=${cache_url[caches]}
      cache_sec[i]=${cache_sec[caches]}
      cache_stamp[i]=${cache_stamp[caches]}
      cache_comment[i]=${cache_comment[caches]}
    fi
    cache_modified=1
    return
  fi
done

# not found
abort $LINENO "UNCACHE could not find cache \"$path\""

}



########################################
##  WRITE CACHE
########################################

# update cache entry value for specified file path
# creates new cache enty if necessary
# marks cache modified if update value is different than present value
# syntax: write_cache "$path" "$key" "$value"

# alias paths are not cached

write_cache () {
local path key value i prev

path=$1
key=$2
value=$3

# select cache (create if needed)
select_cache "$path"

# store value (marks cache_modified if new value is different than existing value)
if [ "$key" == "size" ] ; then
  prev="${cache_size[cache_index]}"
  cache_size[cache_index]="$value"
elif [ "$key" == "dims" ] ; then
  prev="${cache_dims[cache_index]}"
  cache_dims[cache_index]="$value"
elif [ "$key" == "rotated" ] ; then
  prev="${cache_rotated[cache_index]}"
  cache_rotated[cache_index]="$value"
elif [ "$key" == "url" ] ; then
  prev="${cache_url[cache_index]}"
  cache_url[cache_index]="$value"
elif [ "$key" == "sec" ] ; then
  prev="${cache_sec[cache_index]}"
  cache_sec[cache_index]="$value"
elif [ "$key" == "stamp" ] ; then
  prev="${cache_stamp[cache_index]}"
  cache_stamp[cache_index]="$value"
elif [ "$key" == "offset" ] ; then
  prev="${cache_offset[cache_index]}"
  cache_offset[cache_index]="$value"
elif [ "$key" == "comment" ] ; then
  prev="${cache_comment[cache_index]}"
  cache_comment[cache_index]="$value"
else
  abort $LINENO "write_cache called with unsupported key \"$key\""
fi

if [ "$value" != "$prev" ] ; then
  debug_cache "cache \"$cache_selected\" value changed from \"$prev\" to \"$value\""
  cache_modified=1
else
  debug_cache "cache \"$cache_selected\" value remains unchanged at \"$value\""
fi

}



########################################
##  WARN
########################################

# print colored warning message to STDERR - also beep once

warn () {

print_red "$1"$'\a' 1>&2

}





################################################################################
###
###  PARSE SWITCHES
###
################################################################################

while [ "${1:0:1}" == "-" ] ; do

  if [ "$1" == "-debug" ] ; then
    shift
    switch_debug=1
    debug "general debugging enabled"

  elif [ "$1" == "-debug_comment" ] ; then
    shift
    switch_debug_comment=1
    debug_comment "comments debugging enabled"

  elif [ "$1" == "-debug_cache" ] ; then
    shift
    switch_debug_cache=1
    debug_cache "cache debugging enabled"

  elif [ "$1" == "-rebuild_caches" ] ; then
    shift
    switch_rebuild_caches=1
    echo "will rebuild caches"

  elif [ "$1" == "-dryrun" ] ; then
    shift
    switch_dryrun=1
    echo "this will be a dry-run"

  else
    abort $LINENO "encountered unsupported switch \"$1\""
  fi
done





################################################################################
################################################################################
##
##  START
##
################################################################################
################################################################################


########################################
##  STARTUP
########################################

debug "Make WWW Index version $vers"
debug
debug


##  SELECT FOLDER TO INDEX

root=${1:-}
if [ "${root:${#root}-1}" == "/" ] ; then
  # remove trailing "/"
  root=${root:0:${#root}-1}
fi
if [ -z "$root" ] ; then
  abort $LINENO "specify folder to create index for"
elif ! [ -d "$root" ] ; then
  abort $LINENO "root folder not found: \"$root\""
fi
root_athumbs="${root}/${athumb_folder}"


##  CHANGE TO SPECIFIED FOLDER

cd "$root" || true


##  ABORT IF "DO NOT INDEX" FILE IS IN THIS FOLDER

if [ -f "$do_not_index_file" ] ; then
  abort $LINENO "DO NOT INDEX file found in this folder"
fi


##  DELETE DEFUNCT FLAG FILES

remove ".gallery" 1
remove ".vgallery" 1


##  LOAD CACHE

load_cache


##  GENERATE PAGE TITLE

title=${root##*/}
#echo "$root : title = \"$title\""
echo "$root : "$'\e[1;94;102m'" $title "$'\e[m'  #  path : (title in bold blue text on bright green background)


##  CREATE NEW INDEX FILE

debug "creating new index"
echo -n > "$new_index"
append "<head></head>"
append "<body>"




########################################
##  RENDER TOP
########################################

##  RENDER BACK

if ! [ -e "$do_not_back_file" ] ; then
  back=${root%/*}
  back=${back##*/}
  append "<a href=\"..\index.html\">back to ${back}</a>" 1
  BR
fi


##  RENDER TITLE

append "<title>$(armortext "$title")</title>"
append "<font size=\"+4\">$title:</font>"
BR
BR
BR


##  RENDER BANNER PICTURE IF FOUND

b= ; b=$(ls "${bannerpic}."* 2> /dev/null)
if [ -z "$b" ] ; then
  debug "banner picture not found at \"${bannerpic}.*\""
else
  b=${b%%$'\n'*}
  debug "rendering banner picture \"$b\""
  append "<img src=\"$b\">"
  BR
  BR
fi


##  RENDER BANNER TEXT IF FOUND

if ! [ -f "$bannertext" ] ; then
  debug "banner text source not found at \"$bannertext\""
else
  debug "rendering banner text"
  #b=$(cat "$bannertext" | tr '\n' '&' | sed 's/&/<BR>&/g' | tr '&' '\n')
  b=$(cat "$bannertext")
  append "<font size=\"+2\">$(armortext "$b" 1)</font>"  # make web safe, but allow certain embedded HTML to survive
  BR
  BR
fi



########################################
##  PREPARE FOR CLICKABLES
########################################


##  BUILD LIST OF FILES IN THIS FOLDER

hits=0
ls | grep -v "^\\(${full_folder}\|${thumb_folder}\|${athumb_folder}\|${max_folder}\|${videos_folder}\|index.html\|$bannerpic.*\|$bannertext\\)$" | grep -v "~" | sort -f > "$temp_file"
while IFS=$'\n' read -r data; do
  hit[hits++]="$data"
done < "$temp_file"
remove "$temp_file" 1

# add videos in videos folder (if present)
if [ -d "$videos_folder" ] ; then
  #ls "$videos_folder" | grep "\.\(avi\|AVI\|mov\|MOV\|mp4\|MP4\)$" | sort -f > "$temp_file"
  ls "$videos_folder" | grep "\.\(avi\|AVI\|mov\|MOV\|mp4\|MP4\)\( alias\)\?$" | sort -f > "$temp_file"
  while IFS=$'\n' read -r data; do
    hit[hits++]="${videos_folder}/$data"  #  these hits will be prefixed with "videos/"
  done < "$temp_file"
  remove "$temp_file" 1
fi


## ENABLE IMAGE GALLERY IF MINIMUM OF IMAGES ARE PRESENT AND "no image gallert" file is absent

ic=$(($(ls "$max_folder" 2> /dev/null | wc -l)))
if [ -f "${noigallery_file}" ] ; then
  debug "image gallery manually disabled for this folder ($ic images found)"
  igalpics=
elif [ "$ic" -ge "$gallery_minfiles" ] ; then
  debug "image gallery enabled ($ic images found)"
  igalpics=0
else
  debug "image gallery disabled (ivc image found)"
  igalpics=""
fi


## ENABLE VIDEO GALLERY IF MINIMUM OF VIDEOS ARE PRESENT AND "no video gallery" file is absent

vc=$(($(ls | grep "\.\(avi\|AVI\|mov\|MOV\|mp4\|MP4\)$" | wc -l)))
if [ -f "${novgallery_file}" ] ; then
  debug "video gallery manually disabled for this folder ($vc videos found)"
  vgalpics=
elif [ "$vc" -ge "$gallery_minfiles" ] ; then
  debug "video gallery enabled ($vc videos found)"
  vgalpics=0
elif [ -d "${root}/${videos_folder}" ] ; then
  debug "video gallery enabled (videos folder present)"
  vgalpics=0
else
  debug "video gallery disabled ($vc videos found)"
  vgalpics=""
fi




default_ath () {
local label ath_file ath_url match_at match_filetype m noback apre
label=$1
noback=$2

# global_athumb and global_athumb_url are defined by caller

if [ -z "$noback" ] ; then
  apre="../"
fi

if [ -n "$ath" ] ; then
  # we already have an ath, no need to look for a default
  echo "ALREADY HAVE ATH FOR \"$label\" (\"$ath\")"
  return
fi
debug "BLANK ATH FOR \"$label\", CHECK FOR GLOBAL IN \"$global_athumb\"..."
ath_file="${global_athumb}/${b}.png"

debug "MATCHING label=\"$label\""
match_filetype=${label##*.}
if [ "$match_filetype" == "$label" ] ; then
  match_filetype=""
fi
m=${label%).webloc}
if [ "$m" == "$label" ] ; then
  match_at=""
else
  match_at=${m#* at }
  if [ "$match_at" == "$m" ] ; then
    match_at=${m#* (at }
    if [ "$match_at" == "$m" ] ; then
      match_at=""
    fi
  fi
fi


debug "match_filetype=\"$match_filetype\""
debug "match_at=\"$match_at\""
 
ath_file="${global_athumb}/${match_filetype}.png"
if [ -f "$ath_file" ] ; then
  # found a global athumb for this file type


debug "apre=\"$apre\""
debug "global_athumb_url=\"$global_athumb_url\""
debug "match_filetype=\"$match_filetype\""


  ath="${apre}${global_athumb_url}/${match_filetype}.png"
  debug "FOUND GLOBAL FILETYPE ATHUMB AT \"$ath_file\", MAPS TO ATH \"$ath\""
  return
else
  debug "NO GLOBAL FILETYPE ATHUMB AT \"$ath_file\""
fi

ath_file="${global_athumb}/${match_at}.png"
if [ -f "$ath_file" ] ; then
  # found a global athumb for this URL "at" (website)
  ath="${apre}${global_athumb_url}/${match_at}.png"
  debug "FOUND GLOBAL AT-URL ATHUMB AT \"$ath_file\", MAPS TO ATH \"$ath\""
  return
else
  debug "NO GLOBAL AT-URL ATHUMB AT \"$ath_file\""
fi

ath_file="${global_athumb}/URL.png"
if [[ ("${label%).webloc}" != "$label") && (-f "$ath_file") ]] ; then
  # found a global athumb for generic URL
  ath="${apre}${global_athumb_url}/URL.png"
  debug "FOUND GLOBAL ANY-URL ATHUMB AT \"$ath_file\", MAPS TO ATH \"$ath\""
  return
else
  debug "NO GLOBAL ANY-URL ATHUMB AT \"$ath_file\""
fi



debug "NO GLOBAL ATHUMB AVAILABLE FOR \"$label\" IN \"$global_athumb\""
}





########################################
##  PROCESS CLICKABLES
########################################


if [ "$hits" == "0" ] ; then
  debug "no clickable links found"
else
  debug "rendering $hits clickable links"
  for ((i=0;i<hits;i++)) ; do
    p=${hit[i]}

    name=${p%.*}
    pnoalias=${p% alias}
    ext=${pnoalias##*.}
    tag=${name//://}  #  Finder slashes appear as colons in unix, so change ':' to '/' for the tag
    path="${root}/$p"
    debug "processing item $i: $p"

    if [ "$p" != "$pnoalias" ] ; then
      isalias=1
      atp=$(alias_to_path "$root/$p")
    else
      isalias=
    fi

    if [[ ("$ext" == "mp4") || ("$ext" == "MP4") || ("$ext" == "avi") || ("$ext" == "AVI") ]] ; then
      islowvideo=1
    else
      islowvideo=
    fi

    if [[ ("$ext" == "mp4") || ("$ext" == "MP4") || ("$ext" == "avi") || ("$ext" == "AVI")  || ("$ext" == "mov") || ("$ext" == "MOV") ]] ; then
      isvideo=1
    else
      isvideo=
    fi

#    if [[ ("$ext" == "jpg") || ("$ext" == "JPG") || ("$ext" == "png") || ("$ext" == "PNG") || ("$ext" == "jpeg") || ("$ext" == "JPEG") || ("$ext" == "gif") || ("$ext" == "GIF") || ("$ext" == "tif") || ("$ext" == "TIF") || ("$ext" == "tiff") || ("$ext" == "TIFF") || ("$ext" == "bmp") || ("$ext" == "BMP" )]] ; then
#      isimage=1
#    else
#      isimage=
#    fi


    ##  RENDER WEB BROWSER BOOKMARKS AS CLICKABLE LINKS

    if [ "$ext" == "webloc" ] ; then

      get_url "$path"

      # check for presence of bookmark image in athumbs folder
      b="$name"
      if [ -d "$athumb_folder" ] ; then
        ath=$(ls "$athumb_folder" | grep "^${b}\." | grep -i "\.\(JPG\|jpg\|png\|jpeg\|gif\|png\|tif\|TIFF\|bmp\|BMP\)$" | head -n1)
      else
       ath=""
      fi
      #print_yellow "ath = \"$ath\" "
      # if no custom athumb, check for global athumb
      default_ath "$p"

      if [ -z "$ath" ] ; then
        # no thumbnail available for this folder

        # add clickable link
        if [ $open_bookmarks_in_new_tab ] ; then
          append "<a href=\"$got_url\" alt=\"click to open in new tab/window\" target=\"_blank\">$(armortext "$tag")</a>" 1
        else
          append "<a href=\"$got_url\">$(armortext "$tag")</a>" 1
        fi

      else

        # add thumbnail and clickable link

        #if [ $open_bookmarks_in_new_tab ] ; then
        #  append "<a href=\"$got_url\" alt=\"click to open in new tab/window\" target=\"_blank\">$(armortext "$tag")</a>" 1
        #else
        #  append "<a href=\"$got_url\">$(armortext "$tag")</a>" 1
        #fi

        pe="$got_url"
        bb="$name"
        bb=${bb//://}
        b="${athumb_folder}/${ath}"
        if [ $open_bookmarks_in_new_tab ] ; then
          append "<a href=\"$pe\" alt=\"click to open in new tab/window\" target=\"_blank\"><img src=\"$(armorurl "$b")\"></a>" 1
          append "<a href=\"$pe\" alt=\"click to open in new tab/window\" target=\"_blank\">$(armortext "$bb")</a>" 1
        else
          append "<a href=\"$pe\"><img src=\"$(armorurl "$b")\"></a>" 1
          append "<a href=\"$pe\">$(armortext "$bb")</a>" 1
        fi

      fi

      BR
      continue
    fi

    # this clicable item could be a video, a PDF, or something else.  it might be large.


    ##  DON'T RENDER LOWER QUALITY ENCODES IF A HIGHER QUALITY ENCODE IS AVAILABLE

    if [[ ("$islowvideo") && (-f "${name}.MOV") ]] ; then
      # this is an mp4/avi, and there's a mov with the same name, so skip this one (it's a poorer quality / larger encode of the same video)
      debug "skipping lower quality video file \"$p\""
      continue
    fi


    ##  RENDER FOLDER ALIAS TO OPEN

    # don't cache folder aliases because they might change

#    if [[ ("$isalias") && (-d "$p") ]] ; then  #  I don't know why I was checking for P being a directory, it should never be one if it's an alias??
    if [ "$isalias" ] ; then
      # file extensions were stripped off name and tag, but this is a folder with no extensions, so we need to rebuild that
      tag=${p% alias}  # strip " alias" off end of tag
      b=${p% alias}
      k=$(alias_to_link "$p")
      if [ -n "$k" ] ; then
        # alias isn't broken
        # determine source folder
        sf=${k%/*}
        sf=${sf##*/}
        if [ "$sf" == "." ] ; then
          # backing up and only going forward one folder
          sf=$(cd "${k%/*}" || exit ; pwd)
          sf=${sf##*/}
        fi
        b=${b//://}
        # look for (local) alias thumbnail
        if [ -d "$athumb_folder" ] ; then
          ath=$(ls "$athumb_folder" | grep "^${b}\." | grep -i "\.\(JPG\|jpg\|png\|jpeg\|gif\|png\|tif\|TIFF\|bmp\|BMP\)$" | head -n1)
        else
         ath=""
        fi

        # look for (remote) alias thumbnail
        if [ -n "$ath" ] ; then
          # have a local thumb, don't look for a remote thumb
          rath=""
        else
          # no local thumb, look for a remote thumb
          rathumb_folder="${k%/*}/${athumb_folder}"
          #print_red "rathumb_folder = \"$rathumb_folder\""
          if [[ (-d "$rathumb_folder") && (! -f "$do_ignore_alias_athumb") ]] ; then
            # alias has an athumb folder, and we're allowed to use it
            # rath=$(ls "$rathumb_folder" | grep "^${b}\." | grep -i "\.\(JPG\|jpg\|png\|jpeg\|gif\|png\|tif\|TIFF\|bmp\|BMP\)$" | head -n1)
            # that only works if the alias's name is the same as the original's name
            rath=$(ls "$rathumb_folder" | grep "^${k##*/}\." | grep -i "\.\(JPG\|jpg\|png\|jpeg\|gif\|png\|tif\|TIFF\|bmp\|BMP\)$" | head -n1)
          else
           rath=""
          fi
          if [ -n "$rath" ] ; then
            #print_red "rath = \"$rath\""
            test
          fi
        fi

        # local folder reference need to reference the index file otherwise Safari will hand the folder path to Findr to display
        k9="${k}/index.html"
        #echo "k9 = \"$k9\""
        #echo "pwd = \"$(pwd)\""
        if [ -f "$k9" ] ; then
          k="$k9"
          #echo "YES"
        #else
          #echo "NO"
        fi
        #exit 1

        #print_blue "b = \"$b\""

        if [[ (-z "$ath") && (-z "$rath") ]] ; then
          # no local/remote thumbnail available for this alias
          if [ $open_aliases_in_new_tab ] ; then
            append "<a href=\"$(armorurl "$k")\" alt=\"click to open in new tab/window\" target=\"_blank\">$(armortext "$tag") (in $sf)</a>" 1
          else
            append "<a href=\"$(armorurl "$k")\">$(armortext "$tag") (in $sf)</a>" 1
          fi
        else
          # thumb (130 pixel high usually) available for this alias
          bb="$b"
          if [ $open_aliases_in_new_tab ] ; then

            if [ -z "$rath" ] ; then
              # using local thumbnail
              b="./${athumb_folder}/$ath"
            else
              # using remote thumbnail
              b="./${rathumb_folder}/$rath"
              #print_green "b = \"$b\""
            fi

            append "  <a href=\"$(armorurl "$k")\" alt=\"click to open in new tab/window\" target=\"_blank\"><img src=\"$(armorurl "$b")\"></a>" 1
            append "  <a href=\"$(armorurl "$k")\" alt=\"click to open in new tab/window\" target=\"_blank\">$(armortext "$bb") (in $sf)</a>" 1
          else
            append "  <a href=\"$(armorurl "$k")\" target=\"_blank\"><img src=\"$(armorurl "$b")\"></a>" 1
            append "  <a href=\"$(armorurl "$k")\" target=\"_blank\">$(armortext "$bb") (in $sf)</a>" 1
          fi
        fi
      else
        append "(broken alias '$p')" 1
      fi
      BR
      continue
    fi


    ##  RENDER FOLDER TO OPEN

    if [ -d "$p" ] ; then
      # do not generate a link to folders that are supposed to be hidden
      if [ -e "${p}/${hide_file}" ] ; then
        # folder contains hide marker file, no link will be generated, but it will still be synced, so you can only visit it if you know the url
        # it's also still getting backed up to the server
        continue
      fi
      b=$p

      # check for presence of folder image in athumbs folder
      if [ -d "$athumb_folder" ] ; then
        ath=$(ls "$athumb_folder" | grep "^${b}\." | grep -i "\.\(JPG\|jpg\|png\|jpeg\|gif\|png\|tif\|TIFF\|bmp\|BMP\)$" | head -n1)
      else
       ath=""
      fi

      #  if object has a Finder comment, add it to the end of the link descriotion
      b=${b//://}
      get_comment "$p"
      if [ -n "$got_comment" ] ; then
        b="${b} ($got_comment)"
      fi

      if [ -z "$ath" ] ; then
        # no thumbnail available for this folder
        pe="$p"
        dnif="${p}/${do_not_index_file}"
        if [ -e "$dnif" ] ; then
          # this folder should not be indexed
          dni="${p}/index.html"
          if [ -e "$dni" ] ; then
            # use existing index
            pe="$dni"
          else
            # allow the folder to auto-index
            test
          fi
          if [ "$open_folder_in_new_tab" ] ; then
            append "<a href=\"$(armorurl "$pe")\" alt=\"click to open in new tab/window\" target=\"_blank\">$(armortext "$b")</a>" 1
          else
            append "<a href=\"$(armorurl "$pe")\">$(armortext "$b")</a>" 1
          fi
        else
          # point to the index file we will be making
          if [ "$open_folder_in_new_tab" ] ; then
            append "<a href=\"$(armorurl "$pe")/index.html\" alt=\"click to open in new tab/window\" target=\"_blank\">$(armortext "$b")</a>" 1
          else
            append "<a href=\"$(armorurl "$pe")/index.html\">$(armortext "$b")</a>" 1
          fi
        fi

      else

        # thumb (130 pixel high usually) available for this folder

        ##  render clickable image with clickable link below it

        pe="$p"
        dnif="${p}/${do_not_index_file}"
        bb="$b"
        b="./${athumb_folder}/$ath"
        if [ ! -e "$dnif" ] ; then
          # this folder should be indexed, so specify the index file (let it auto index, or use ay pre-existing index file)
          pe="${pe}/index.html"
        fi

        # point to the index file we will be making
        if [ "$open_folder_in_new_tab" ] ; then
          append "<a href=\"$(armorurl "$pe")\" alt=\"click to open in new tab/window\" target=\"_blank\"><img src=\"$(armorurl "$b")\"></a>" 1
          append "<a href=\"$(armorurl "$pe")\" alt=\"click to open in new tab/window\" target=\"_blank\">$(armortext "$bb")</a>" 1
        else
          append "<a href=\"$(armorurl "$pe")\"><img src=\"$(armorurl "$b")\"></a>" 1
          append "<a href=\"$(armorurl "$pe")\">$(armortext "$bb")</a>" 1
        fi

      fi
      BR
      continue
    fi


    ##  RENDER ANIMATED GIFS INLINE

    if [[ (("$ext" == "gif") || ("$ext" == "GIF")) ]] ; then  # assume all GIFs are animations to be rendered inline
      n=${p##*/}
      n=${n%.*}
      append "${n}:" 1
      append "  <img src=\"$(armorurl "$p")\"></a>" 1
      BR
      continue
    fi


    ##  PREPARE TO RENDER AN INLINE CLICKABLE LINK (or it may be a video that goes into a gallery)

    b=${p#*/}  # removes videos/ prefix

    # check to see if whatever it is has a thumbnail picture available
    if [ -d "$athumb_folder" ] ; then
      bbase=${b%.*}
      # armor bbase for grep
      bbase=$(echo "$bbase" | sed 's/\[/\\\[/g' | sed 's/\]/\\\]/g')
      ath=$(ls "$athumb_folder" | grep "^${bbase}\." | grep -i "\.\(JPG\|jpg\|png\|jpeg\|gif\|png\|tif\|TIFF\|bmp\|BMP\)$" | head -n1)
    else
      ath=""
    fi

    targ=$(armorurl "$b")
    b=${b%.html}
    b=${b%.HTML}
    b=${b% alias}

    # add file size if it's greater than the warn size
    if [ "$isalias" ] ; then
      bytes=$(stat -f "%z" "$atp") || bytes=0
    else
      bytes=$(stat -f "%z" "$p") || bytes=0
    fi
    ((kb=bytes/1024))
    if [ "$kb" -ge "$size_warn_limit_kb" ] ; then
      if [ "$kb" -gt "2047" ] ; then
        ((mb=kb/1024))
        b="${b} ($mb MB)"
      else
        b="${b} ($kb KB)"
      fi
    fi

    #  if object has a Finder comment, add it to the end of the link descriotion
    #b=${b//://}
    #get_comment "$mfile"
    #if [ -n "$got_comment" ] ; then
    #  b="${b} ($got_comment)"
    #fi


    ##  CREATE THUMBNAILS FOR VIDEOS

    ath="${athumb_folder}/${ath}"

    pl="${root}/${p}"
    if (echo "$pl" | grep -q -i "\.\(mp4\|mov\|avi\)$") ; then

      #  check to see if thumb looks like it has a portrait dimension
      if [ -f "$ath" ]  ; then
        get_dims "$ath"
        w=${got_dims%,*}
        h=${got_dims#*,}
        ((ratio=100*h/w))
        if [ "$ratio" -ge "$portrait_thumb_pct" ] ; then
          info "dimensions ($got_dims) of thumb at \"$ath\" appear to be portrait, V/H ratio = $ratio %"
          if [ $switch_rebuild_portrait_thumbs ] ; then
            info "deleting portrait thumb at \"$ath\""
            remove "$ath" 1
          fi
        fi
      fi

      if ! [ -f "$ath" ] ; then
        # athumb not found (entire athumb folder may not exit)
        ath=${p##*/}
        ath="${ath%.*}.PNG"
        ath="${root}/${athumb_folder}/${ath}"
        if [ "$switch_dryrun" ] ; then
          dryrun "would have called extract_video_thumbnail to extract thumbnail from video \"$pl\""
        else

#goto

          extract_video_thumbnail "$pl" "$ath"
          if [ $rc != 0 ] ; then
            abort $LINENO "extract_video_thumbnail returned rc=$rc"
          fi
        fi
        ath=${p##*/}
        ath="${athumb_folder}/${ath%.*}.PNG"
      fi
    fi

    if [ -f "$ath" ] ; then
      oath="$ath"
      ath=$(armorurl "$ath")
    else
      oath=""
      ath=""
    fi

    descrip=$(armortext "$b")



   ##  ADD VIDEOS TO VIDEO GALLERY IF ENABLED

    if [ "$isvideo" ] ; then
      # it's a video (MP4 / AVI / MOV)

      if [ -n "$vgalpics" ] ; then
        # vgallery mode is enabled

        if [ "$isalias" ] ; then
          # it's an alias to a video somewhere else
          b="${b% alias}"
          k=$(alias_to_link "$p")
          if [ -n "$k" ] ; then
            sf=${k%/*}
            sf=${sf##*/}
            if [ "$sf" == "." ] ; then
              # backing up and only going forward one folder
              sf=$(cd "${k%/*}" || exit ; pwd)
              sf=${sf##*/}
            fi
            b=${b//://}
            b=${b%.*}

kk=${k%/*}
kk=${kk%/*}
kk="${kk}/${athumb_folder}"

echo "root/p=\"$root/$p\""
echo "sf=\"$sf\""
echo "k=\"$k\""
echo "kk=\"$kk\""
atf=${atp%/*}
atf=${atf%/*}
atf="${atf}/${athumb_folder}"
echo "atf=\"$atf\""
echo "b=\"$b\""

#exit 1

            # look for alias thumbnail
            if [ -d "$atf" ] ; then
              ath=$(ls "$atf" | grep "^${b}\." | grep -i "\.\(png\|PNG\)$" | head -n1)
            else
             ath=""
            fi

echo "ath=\"$ath\""
#exit 1



            vgaltag[vgalpics]="&nbsp;&nbsp;${descrip}&nbsp;&nbsp;"
            vgalthumb[vgalpics]="${kk}/${ath}"  # (relative) thumb for videos is in athumb folder, not thumb folder
            vgalurl[vgalpics]=$(armorurl "$k")
            vgaltfile[vgalpics]="${atf}/${ath}" # full path to thumb

#echo
#echo "vgaltag[$vgalpics]   = \"${vgaltag[vgalpics]}\""
#echo "vgalthumb[$vgalpics] = \"${vgalthumb[vgalpics]}\""
#echo "vgalurl[$vgalpics]   = \"${vgalurl[vgalpics]}\""
#echo "vgaltfile[$vgalpics] = \"${vgaltfile[vgalpics]}\""
#echo
#exit 1

          else
            append "(broken alias '$p')" 1
          fi


        else
          #  it's not an alias
          vgaltag[vgalpics]="&nbsp;&nbsp;${descrip}&nbsp;&nbsp;"
          vgalthumb[vgalpics]="$ath"  # (relative) thumb for videos is in athumb folder, not thumb folder
          vgalurl[vgalpics]=$(armorurl "$p")
          vgaltfile[vgalpics]="${root}/${oath}" # full path to thumb

        fi

        ((vgalpics++))
        continue  # skip over creating an inline video with description
      fi
    fi


    ##  RENDER WHATEVER IS LEFT AS AN INLINE CLICKABLE (possibly with size indicator)

    # if no custom athumb, check for global athumb
    #mkdir -p "athumb"
    default_ath "$p" 1

    if [ -z "$ath" ] ; then
      # no thumbnail available for this object
      if [ $open_aliases_in_new_tab ] ; then
        append "<a href=\"${targ}\" alt=\"click to open in new tab/window\" target=\"_blank\">${descrip}</a>" 1
      else
        append "<a href=\"${targ}\">${descrip}</a>" 1
      fi
    else
      # thumb (130 pixel high usually) available for this object
      ##  render clickable alias image
      if [ $open_documents_in_new_tab ] ; then
        append "  <a href=\"${targ}\" alt=\"click to open in new tab/window\" target=\"_blank\"><img src=\"${ath}\"></a>" 1
        append "  <a href=\"${targ}\" alt=\"click to open in new tab/window\" target=\"_blank\">${descrip}</a>" 1
      else
        append "  <a href=\"${targ}\" target=\"_blank\"><img src=\"${ath}\"></a>" 1
        append "  <a href=\"${targ}\" target=\"_blank\">${descrip}</a>" 1
      fi
    fi
    BR
  done
fi
BR



########################################
##  RENDER INLINE IMAGES
########################################

if [ -d "$max_folder" ] ; then

  ##  BUILD LIST OF IMAGES

  ls "$max_folder" | grep -i "\.\(JPG\|jpg\|png\|jpeg\|gif\|png\|tif\|TIFF\|bmp\|BMP\)$" | sort -f > "$temp_file"
  hits=0
  while IFS=$'\n' read -r data; do
    hit[hits++]="$data"
  done < "$temp_file"
  remove "$temp_file" 1
  debug "rendering $hits clickable thumbnails"

  ## LOOP THROUGH MAX IMAGES, CREATING FULL AND THUMB FILES - RENDER INLINE IF GALLERY IS NOT ACTIVE

  for ((i=0;i<hits;i++)) ; do

    #  grab one max image
    f=${hit[i]}
    prefix="${f%.*}"
    debug "adding index $i: $prefix"
    t="${thumb_folder}/$f"
    p="${full_folder}/$f"

    #  create thumb and full folders if needed
    mkdir -p "${root}/${full_folder}"
    mkdir -p "${root}/${thumb_folder}"
   
    # define max/full/thumb paths since appescript will requrie full paths
    mfile="${root}/${max_folder}/${f}"
    ffile="${root}/${full_folder}/${f}"
    tfile="${root}/${thumb_folder}/${f}"

    # we'll need a temp file for scaling, and it needs to be a full path
    temp_pic="${root}/.temp.jpg"

    #get dimensions of MAX image
    rfile="${root}/${do_not_redim}"
    if [ -e "$rfile" ] ; then
      # get max picture's dimensions
      get_dims "$mfile" $LINENO
    else
      # get max picture's dimensions (adjust dimensions if needed)
      get_dims_standardized "$mfile" $LINENO
    fi

    #  create full if it doesn't already exist
    if ! [ -f "$ffile" ]  ; then
      echo "creating FULL for \"$f\""

      # determime amount image needs to be scaled
      width=${got_dims%,*}  # "1024"
      height=${got_dims#*,}  # "768"
      ((vpct=full_vlimit*10000/height))  # "48"   ie  48%, or factor 0.48
      ((hpct=full_hlimit*10000/width))  # "48"   ie  48%, or factor 0.48

      # adjust for vertical fit
      if [ "$vpct" -lt "$hpct" ] ; then
        pct="$vpct"
      else
        pct="$hpct"
      fi

      #  create full copy at max size
      cp "$mfile" "$temp_pic"
      assert $LINENO "create full copy at max size" "cp \"$mfile\" \"$temp_pic\""

      # use Image Events to scale full if needed - might change this to ffmpeg later
      if [ "$pct" -lt "10000" ] ; then
        osascript -e "set unix_path to \"$temp_pic\"" -e "set pct to $pct" -e "set fac to pct/10000" -e "tell application \"Image Events\"" -e "set img to open unix_path" -e "scale img by factor fac" -e "save img" -e "close img" -e "end tell"
        assert $LINENO "scale full" "osascript -e \"set unix_path to \\\"$temp_pic\\\"\" -e \"set pct to $pct\" -e \"set fac to pct/10000\" -e \"tell application \\\"Image Events\\\"\" -e \"set img to open unix_path\" -e \"scale img by factor fac\" -e \"save img\" -e \"close img\" -e \"end tell\""
      fi
      # move new full picture file into position
      if [ "$switch_dryrun" ] ; then
        dryrun "would have installed new image full at \"$ffile\""
        remove "$temp_pic" 2
      else
        mv "$temp_pic" "$ffile"
        assert $LINENO "move new full picture file into position" "mv \"$temp_pic\" \"$ffile\""
      fi
    fi

    #  check to see if thumb looks like it has a portrait dimension
    if [ -f "$tfile" ]  ; then
      get_dims "$tfile"
      w=${got_dims%,*}
      h=${got_dims#*,}
      ((ratio=100*h/w))
      if [ "$ratio" -ge "$portrait_thumb_pct" ] ; then
        info "dimensions ($got_dims) of thumb at \"$tfile\" appear to be portrait, V/H ratio = $ratio %"
        if [ $switch_rebuild_portrait_thumbs ] ; then
           info "deleting portrait thumb at \"$tfile\""
          remove "$tfile" 1
        fi
      fi
    fi

    #  create thumb if it doesn't already exist
    if ! [ -f "$tfile" ]  ; then
      echo "creating THUMB for \"$f\""
      extract_image_thumbnail "$mfile" "$tfile"
    fi

    #  if max image file has a Finder comment, add it to the prefix
    prefix=${prefix//://}
    get_comment "$mfile"
    if [ -n "$got_comment" ] ; then
      prefix="${prefix} ($got_comment)"
    fi

    # if gallery mode, add this image to the gallery array
    if [ -n "$igalpics" ] ; then
      galtag[igalpics]="&nbsp;&nbsp;$(armortext "${prefix}")&nbsp;&nbsp;"
      galthumb[igalpics]=$(armorurl "$t")
      galurl[igalpics]=$(armorurl "$p")
      galtfile[igalpics]="$tfile"
      ((igalpics++))
      continue  # skip over creating an inline thumb with description
    fi

    #  render one click-zoomable image
    append "$(armortext "${prefix}"):" 1
    append "  <a href=\"$(armorurl "$p")\" target=\"_blank\"><img src=\"$(armorurl "$t")\"></a>" 1
    # I wasn't able to find a way to get the ALT to work, it's not that important

    #  add matching clickable URL if present in MAX folder
    p="${p%.*}.webloc"
    if [ -f "$p" ] ; then
      # create tag
      b=${p##*/}
      b=${b%.webloc}

      # get url
      get_url "${root}/${p}"

      # if URL file has a Finder comment, set the tag to that instead
      b=${b//://}
      get_comment "${root}/${p}"
      # echo "comment for \"$p\"is \"$got_comment\"" 1>&2
      if [ -n "$got_comment" ] ; then
        #b="${b} ($got_comment)"
        b="$got_comment"
      fi

      if [ $open_bookmarks_in_new_tab ] ; then
        append "<a href=\"$got_url\" alt=\"click to open in new tab/window\" target=\"_blank\">$(armortext "$b")</a>" 1
      else
        append "<a href=\"$got_url\">$(armortext "$b")</a>" 1
      fi
    fi

    BR
  done
fi



########################################
##  RENDER IMAGE GALLERY
########################################

debug "igalpics=\"$igalpics\""
if [ -n "$igalpics" ] ; then
  # gallery view was requested, and we have some pictures to render
  append "<TABLE BORDER=\"3\">"
  for ((ii=0;ii<igalpics;ii+=6)) ; do
    # add row of image thumbs
    append "  <TR>"
    for ((i=ii;i<(ii+6);i++)) ; do
      debug "gallery $i/$igalpics"
      append "    <TD WIDTH=\"$((image_thumbnail_width+2))\" ALIGN=CENTER>"
      if [ $i -lt $igalpics ] ; then #  I like how this looks better, leaving "blanks" rather than trying to omit them
        #  get resolution of thumbnail
        get_dims "${galtfile[i]}" $LINENO
        #  warn if thumb looks like it has a portrait dimension
        w=${got_dims%,*}
        h=${got_dims#*,}
        ((ratio=100*h/w))
        if [ "$ratio" -ge "$portrait_thumb_pct" ] ; then
          debug "portrait thumb at \"${galtfile[i]}\", dimension ($got_dims) ratio = $ratio %"
        fi
        # add entry
        append "      <A HREF=\"${galurl[i]}\" alt=\"click to open in new tab/window\" target=\"_blank\">"
        append "        <IMG SRC=\"${galthumb[i]}\" ALT=\"${galtag[i]}\" WIDTH=\"${got_dims%,*}\" HEIGHT=\"${got_dims#*,}\" BORDER=0></A>" 1
        append "      </A>"
      fi
      append "    </TD>"
    done
    append "  </TR>"
    # add row of image tags
    append "  <TR>"
    for ((i=ii;i<(ii+6);i++)) ; do
      if [ $i == $igalpics ] ; then
        # reached end of galpix
        append "    <TD ALIGN=CENTER></TD>"
      else
        append "    <TD ALIGN=CENTER>${galtag[i]}</TD>"
      fi
    done
    append "  </TR>"
  done
  append "</TABLE>"
  BR
fi



########################################
##  RENDER VIDEO GALLERY
########################################

debug "vgalpics=\"$vgalpics\""
if [ -n "$vgalpics" ] ; then
  # vgallery view was not disabled, and we have enough videos to render
  append "<TABLE BORDER=\"3\">"
  for ((ii=0;ii<vgalpics;ii+=6)) ; do
    # add row of video thumbs
    append "  <TR>"
    for ((i=ii;i<(ii+6);i++)) ; do
      debug "vgallery $i/$vgalpics"
      append "    <TD WIDTH=\"$((video_thumbnail_width+2))\" ALIGN=CENTER>"
      if [ $i -lt $vgalpics ] ; then #  I like how this looks better, leaving "blanks" rather than trying to omit them
        #  get resolution of thumbnail
        get_dims "${vgaltfile[i]}" $LINENO # dims saved to got_dims
        #  warn if thumb looks like it has a portrait dimension
        w=${got_dims%,*}
        h=${got_dims#*,}
        ((ratio=100*h/w))
        if [ "$ratio" -ge "$portrait_thumb_pct" ] ; then
          warn "portrait thumb at \"${vgaltfile[i]}\", dimension ($got_dims) ratio = $ratio %"
        fi
        # add entry
        append "      <A HREF=\"${vgalurl[i]}\" alt=\"click to open in new tab/window\" target=\"_blank\">"
        append "        <IMG SRC=\"${vgalthumb[i]}\" ALT=\"${vgaltag[i]}\" WIDTH=\"${got_dims%,*}\" HEIGHT=\"${got_dims#*,}\" BORDER=0></A>" 1
        append "      </A>"
      fi
      append "    </TD>"
    done
    append "  </TR>"
    # add row of image tags
    append "  <TR>"
    for ((i=ii;i<(ii+6);i++)) ; do
      if [ $i == $vgalpics ] ; then
        # reached end of vgalpix
        append "    <TD ALIGN=CENTER></TD>"
      else
        append "    <TD ALIGN=CENTER>${vgaltag[i]}</TD>"
      fi
    done
    append "  </TR>"
  done
  append "</TABLE>"
  BR
fi



########################################
##  RENDER BOTTOM
########################################

##  RENDER LINK TO AUTO-INDEXING MAX FOLDER

if [ -d "${max_folder}" ] ; then
  BR
  if [ "$open_maxfolder_in_new_tab" ] ; then
    append "<font size=\"-2\"><a href=\"$max_folder\" alt=\"click to open in new tab/window\" target=\"_blank\">highest resolution images</a></font>"
  else
    append "<font size=\"-2\"><a href=\"$max_folder\">highest resolution images</a></font>"
  fi
fi


##  RENDER LAST UPDATED

BR
append "<font size=\"-2\">last updated $(date "+%m/%d/%Y at %H:%M:%S") by ${0##*/} version $vers</font>"  # this line is ignored for checksumming when detecting changes to index file


##  FINISH INDEX

append "</body>"
debug "index of $title finished"





################################################################################
################################################################################
##
##  FINISH
##
################################################################################
################################################################################

##  REPLACE INDEX FILE

if [ -f "$index_file" ] ; then
  cs1=$(cat "$index_file" | grep -v "last updated" | openssl md5)  # ignore Last Updated line since it has a constantly-changing timestamp
else
  echo "creating new index for \"$title\""
  cs1=""
fi
cs2=$(cat "$new_index" | grep -v "last updated" | openssl md5)
if [ "$cs1" == "$cs2" ] ; then
  # no change - not replacing file will make rsync make less noise
  remove "$new_index" 1
  debug "(no change to index at \"$title\")"
elif [ "$switch_dryrun" ] ; then
  dryrun "would have installed new index file"
  remove "$new_index" 1
else
  # update it
  if [ -f "$index_file" ] ; then
    remove "$index_file" 1
  fi
  mv "$new_index" "$index_file"
  debug "$cs1 != $cs2"
  info "INDEX UPDATED AT \"$title\""
fi


##  SAVE CACHE FILE IF CHANGED

save_cache


##  DONE

echo



