Image Collection Organizing Helper

#!/bin/bash
#
# sort.sh
# Cycle through all image files in a directory and prompt for a
# directory in which to move them. Useful for organizing downloaded pics
# after a 4chan or ffffound hunt.
#
# Copyright (c) 2011
# Alexandre de Verteuil 2011-07-31
#
# This program is free software. It comes without any warranty, to
# the extent permitted by applicable law. You can redistribute it
# and/or modify it under the terms of the Do What The Fuck You Want
# To Public License, Version 2, as published by Sam Hocevar. See
# http://sam.zoy.org/wtfpl/COPYING for more details.
#
# Optional dependency: rlwrap for tags autocompletion.
# Bugs: Does not work well with directory names containing spaces.
# Usage: sort.sh [files]
# Filename structure: ffffff_tag1[_tagn ...].ext
#     where ffffff are the first 6 characters of the md5sum, useful for
#     autocompleting a filename with just a few characters.

VIEWER="feh --scale-down"
GIFVIEWER="gifview --animate"
TAGSFILE="/home/alex/mm/p/collection/.tags"
RLWRAP=$(which rlwrap 2>/dev/null)
DEBUG=

[ -f "$TAGSFILE" ] || touch "$TAGSFILE"

f_view () {
    # Spawn the appropriate viewer.
    # Return 1 if not a supported file extension.
    if [ ! -f "$candidate" ]; then
        echo Skipping "${candidate}, file doesn't exist..."
        return 1
    fi
    ext=${candidate##*.}  # Get extension.
    ext=${ext,,}  # Lowercase.
    debug file extension "$ext"
    case $ext in
        jpg|jpeg|png) $VIEWER "$candidate" & ;;
        gif) $GIFVIEWER "$candidate";;
        *) echo Skipping "${candidate}, ${ext} not supported..."
           echo
           return 1
           ;;
    esac
    }

debug () {
    if [ "$DEBUG" ]; then
        echo "##DEBUG: "$* >&2
    fi
    }

debug is ON

if [ $# -gt 0 ]; then
    batch="$*"
else
    batch=$(find . -maxdepth 1 -type f -iregex ".*\(jpg\|jpeg\|png\|gif\)")
fi
debug $batch

for candidate in $batch; do
    echo =====================================================
    echo "    Processing $candidate"
    echo '  -------------------------------------------------'
    while :; do
        # Show an image
        f_view "$candidate" || break
        # Print available directories and prompt for a target.
        # Rebuild list of dirs because dirs may be created in the process.
        if [ "$RLWRAP" ]; then
            echo "key in target directory, or again|quit, empty string for ./"
            # This does NOT work if a directory name contains whitespace.
            target=$(rlwrap --one-shot \
                            --substitute-prompt="dir: " \
                            --prompt-colour="Blue" \
                            --file <(find . -mindepth 1 -type d | cut -b 3-) \
                            cat)
            target=${target// /}
            case "$target" in
                ""|"0") targetdir=".";;
                a|again) continue;;
                q|quit) exit;;
                *) if ! [ -d "$target" ]; then
                       mkdir -pv "$target" || exit 1
                   fi
                   targetdir="$target"
            esac
        else
            unset -v dirs
            dirs[0]="."
            i=1
            while read dir; do
                echo $i: $dir
                dirs[$((i++))]="$dir"
            done < <(find . -mindepth 1 -type d -printf '%p\n' | sort) | column
            echo -n "key in target directory, or New|Again|Quit, "
            echo "empty string for ./"
            read -ep "Move to: " target
            case "$target" in
                ""|"0") targetdir=".";;
                n*|N*) read -p "New directory name: " dirname
                     mkdir -pv "$dirname" || exit 1
                     targetdir="$dirname"
                     ;;
                a*|A*) continue;;
                q*|Q*|exit) exit;;
                *) if [ -d "${dirs[$target]}" ]; then
                       targetdir="${dirs[$target]}"
                   fi
                   ;;
            esac
        fi
        echo "Destination directory: ${targetdir}/"
        echo

        # Parse tags from filename
        if [[ "$candidate" =~ (^|^\./)[[:xdigit:]]{5,6}((_[[:alnum:]$&%\!.-]{1,}){1,})\..{3,4}$ ]]; then
            debug BASH_REMATCH is ${BASH_REMATCH[*]}
            buffer=$(tr "_" " " <<< "${BASH_REMATCH[2]:1}")
            for tag in $buffer; do
                if ! grep "^${tag}$" "$TAGSFILE" &>/dev/null; then
                    buffer=$(sed "s/${tag}/\!&/" <<< "${buffer}")
                fi
            done
        else
            buffer=
        fi
        # Read tags from user
        echo -n "Key in tags, space separated, !tags are not added to "
        echo "dictionary, empty string aborts."
        if [ "$RLWRAP" ]; then
            # http://stackoverflow.com/questions/4726695/
            # bash-and-readline-tab-completion-in-a-user-input-loop
            # This explains how read -e only supports default
            # completion.  I use rlwrap, an external program, as a
            # wrapper for readline with my custom word list.
            tags=$(rlwrap --prompt-colour=Yellow \
                          --substitute-prompt="tags: " \
                          --pre-given="$buffer" \
                          --file="$TAGSFILE" \
                          --one-shot \
                          cat)
            debug rlwrap returnded $?
        else
            cat $TAGSFILE | column
            read -ep ": " -i "$buffer" tags
        fi
        if [ ! "$tags" ]; then
            echo Cancelled.
            break
        fi
        debug tags are: \"$tags\"

        # Add new tags to $TAGSFILE
        for tag in $tags; do
            [[ ${tag:0:1} = "!" ]] || echo $tag
        done | cat - $TAGSFILE | sort | uniq > $TAGSFILE.tmp
        mv $TAGSFILE{.tmp,}
        debug TAGSFILE updated.

        # Filename structure :
        tags=$(sed "s/\!\(\w\{1,\}\)/\1/g" <<< "$tags")  # remove '!' prefixes
        tags=$(sed "s/ $//" <<< "$tags")
        tags=$(tr " " "_" <<< "$tags")
        hash=$(md5sum ${candidate})
        hash=${hash:0:6}
        ext=${candidate##*.}  # Get extension.
        ext=${ext,,}  # Lowercase.
        newname="${targetdir}/${hash}_${tags}.${ext}"
        debug newname is: \"$newname\"

        if [ "$candidate" == "$newname" -o "./$candidate" == "$newname" ]; then
            echo No change... Moving on.
        else
            mv -v "$candidate" "$newname" || exit 1
        fi
        echo; echo
        break
    done
    sleep 0.5s
done
Alexandre de Verteuil
Alexandre de Verteuil
Senior Solutions Architect

I teach people how to see the matrix metrics.
Monkeys and sunsets make me happy.

Related