#!/bin/bash
#
# songanizer - script to organize ogg and mp3 files
# Copyright (c) 2002-2004 Patrick Ohnewein.
# All rights reserved.
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#
# Description:
#
# Script to organize a directory containing ogg and mp3 files.
#
# The biggest problem for me, during my efforts to organize music files, was
# the choice of the directory structure. Should the directory structure reflect
# the author or the genre or may be the album? I ended up with the conclusion,
# that no ideal directory structure exists. So I wanted different virtual
# directory structures to the same data. Thanks to the symbolic links
# capability of the file systems I use (ext2, ext3, ...) this dream has become
# reality!
#
# The script gets a list of _data directories, in which the real ogg and mp3
# files reside. The script has to read informations like author, album, genre, 
# ... from these files and create parallel directory structures, which
# just contain symbolic links to the real files.
# 
# The goal is to create virtual directory structures, which give different
# views of the data, but without having redundant copies of the files
# themselves.
#
# dependences:
#  - It's a bash script and therefore needs a running bash.
#  - Uses getopt (enhanced) to extract options from the command line arguments.
#  - Uses gettext (gettext.sh) for i18n support.
#  - The link structures are created using links on the file system and
#    therefore the file system must support them (i.e. ext2, ext3, ...)
#  - To read the mp3 tags the mp3info tool gets used.
#    Get it at http://ibiblio.org/mp3info/
#
# How is the directory structure organized?
#  In the base directory live the following directrories:
#   _data*    all directories, with a name starting with _data, contain the
#             real data (all links will point to its content). These can also
#             be symbolic links to directories on other devices.
#   _artist   contains the link structure on the basis of the artist tag
#   _genre    contains the link structure on the basis of the genre tag
#   _initial  contains the link structure on the basis of the initials
#   ... and more, depending on the switchs passed to the script
#
#
# @version 0.8.20060507    - Make all options singular (fixing broken artist)
#			   - Major reorganizing to separate targets from tags.
#			   - Add new funtions: enableTargets, resolveTag,
#			     resolveTarget
#			   - Add new targets: decade, yeartree, decadetree,
#                            genretree, artisttree, alltree
#			   - Drop target: comments
#			   - Fix bailing out if unable to create temp dir
#			   - Rephrase help text
#			   - Use safe tempdir
# @version 0.8.20060211    - Major reorganizing to loop only once (not per-tag)
# @version 0.8, 2005-11-02 - replaced ls with find [savannah bug #4932 
#			     task #2546]
#			   - there can be multiple levels of sub-directories
#			     inside every data directory [savannah bug #4933
#			     task #2547]
#			   - symbolic links point to individual files instead
#			     of directories
#			   - added the --all switch
#			   - changed the creation algo, so it will first create
#			     the structure in a tmp directory, and only after
#			     completion of this heavy task, in a short
#			     operation the structure is moved to its desired
#			     location 
# @version 0.7, 2004-02-01 - added gettext support for i18n
# @version 0.6, 2004-01-24 - added long options support
# @version 0.5, 2003-08-22 - fixed problem with same album in more than one
#                            data directory [savannah bug #4877]
# @version 0.4, 2003-08-17 - added support for multiple data directories
# @version 0.3, 2002-12-28 - translated all texts into english
# @version 0.2, 2002-12-10 - created generic function organizeOnBaseOfTag()
# @version 0.1, 2002-11-01 - started the project
# @author  Patrick Ohnewein (@lugbz.org)
#
# ToDo:
# - add support for ID3v2 Tags - probably should go into mp3info
#

VERSION="0.8"

# use gettext for internationalization
. gettext.sh

TEXTDOMAIN=songanizer
export TEXTDOMAIN
TEXTDOMAINDIR=$(dirname $0)/../share/locale
export TEXTDOMAINDIR

program_name=$(basename $0)
# definition of the used error codes
EX_OK=0
EX_USAGE=64
EX_SOFTWARE=70
EX_CONFIG=78
if [ -f sysexits ]; then
    # If an external sysexits file exists, we source it. This allows the
    # external overwriting of the error codes.
    source sysexits
fi

ifs="$IFS"
newline='
'

# variables
verbose=0
alltargets="artist artisttree genre genretree year yeartree decade decadetree album"
alltreetargets="artisttree genretree yeartree decadetree"
alltags="artist genre year decade album"
defaulttargets=""

# Prints a version message.
print_version ()
{
	eval_gettext "songanizer, version ${VERSION}"
	echo
	# do not translate copyright notice.
	echo "Copyright (C) 2002-2004 Patrick Ohnewein"
}

# Prints a help message, explaining all the available options.
print_help ()
{
	gettext "Organizes files in one or more virtual directory structures."
	echo
	gettext "Options:"
	echo
	gettext " -A|--artist        Group songs by artist"
	echo
	gettext " --artisttree       Group songs by artist and album"
	echo
	gettext " -G|--genre         Group songs by genre"
	echo
	gettext " --genretree        Group songs by genre, artist and album"
	echo
	gettext " -Y|--year          Group songs by year"
	echo
	gettext " --yeartree         Group songs by year, artist and album"
	echo
	gettext " -D|--decade        Group songs by decade"
	echo
	gettext " --decadetree       Group songs by decade, artist and album"
	echo
	gettext " -L|--album         Group songs by album"
	echo
	gettext " --all              Group songs by any and all of the above"
	echo
	gettext " --alltree          Group songs by any and all of above trees"
	echo
	gettext " -h|--help          Print this help screen"
	echo
	gettext " -v|--verbose       Activate verbose mode"
	echo
	gettext " --version          Print version information"
	echo
}

# Prints a usage message, explaining how the script has to be called.
print_usage ()
{
	eval_gettext "Usage: $program_name [options] basedir"
	echo
	print_help
}

enableTargets () {
	for target in $@; do
		target_is_wanted=""
		for possibletarget in $alltargets; do
			if [ "$possibletarget" = "$target" ]; then
				target_is_wanted="1"
			fi
		done
		if [ -n "$target_is_wanted" ]; then
			for existingtarget in $wantedtargets; do
				if [ "$existingtarget" = "$target" ]; then
					target_is_wanted=""
				fi
			done
			if [ -n "$target_is_wanted" ]; then
				wantedtargets="$wantedtargets $target"
			fi
		else
			eval_gettext "Error ${program_name}: internal error: unknown target \"$target\""
			echo
			exit ${EX_SOFTWARE}
		fi
	done
}

TEMP=$(getopt -o :AGYDLhv --long all,alltree,artist,artisttree,genre,genretree,year,decade,decadetree,album,help,verbose,version -n "$program_name" -- "$@") #"

if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi

eval set -- "$TEMP"

wantedtargets="$defaulttargets"
while true ; do
	case "$1" in
		-A|--artist)	enableTargets artist; shift;;
		--artisttree)	enableTargets artisttree; shift;;
		-G|--genre)	enableTargets genre; shift;;
		--genretree)	enableTargets genretree; shift;;
		-Y|--year)	enableTargets year; shift;;
		--yeartree)	enableTargets yeartree; shift;;
		-D|--decade)	enableTargets decade; shift;;
		--decadetree)	enableTargets decadetree; shift;;
		-L|--album)	enableTargets album; shift;;
		--all)		enableTargets "$alltargets"; shift;;
		--alltree)	enableTargets "$alltreetargets"; shift;;
		-h|--help)	print_help ; exit ${EX_OK} ; shift;;
		-v|--verbose)	verbose=1; shift;;
		--version)	print_version ; exit ${EX_OK} ; shift;;
		--)		shift ; break ;;
		*)		gettext "Unimplemented option choosen. Use -h to visualize a help screen."; echo; exit ${EX_USAGE} ;;
	esac
done

if [ $# -eq 0 ]; then
	print_usage
    exit ${EX_USAGE}
fi

tempdir="`mktemp -tdq \"songanizer.XXXXXXXX\"`"
if [ ! $? -eq 0 ]; then
    eval_gettext "Error ${program_name}: Could not create temporary config file. exiting"
	echo
    exit ${EX_CONFIG}
fi

basedir=${1}
if [ "${basedir:0:1}" != "/" ]; then
    basedir="`pwd`/${basedir}"
fi

if [ ! -d ${basedir} ]; then
    eval_gettext "Error ${program_name}: base directory ${basedir} doesn't exist"
	echo
    exit ${EX_CONFIG}
fi

datadir="${basedir}/_data"

datadirs="${datadir}*"
for datadir_elem in ${datadirs}; do
	if [ ! -d ${datadir_elem} ]; then
		eval_gettext "Error ${program_name}: invalid or no data directory ${datadir_elem}"
		echo
		exit ${EX_CONFIG}
	elif [ $verbose -ne 0 ]; then
		echo "Detected data directory: ${datadir_elem}"
	fi
done

initTempTargetDirs () {
	# pessimistic aproach
	EXIT_CODE=${EX_SOFTWARE}

	IFS="$newline"
	for targetname in $@; do IFS="$ifs";

		temptargetdir="${tempdir}/_${targetname}"

		if [ ! -d "${temptargetdir}" ]; then
			mkdir "${temptargetdir}"
		else
		# remove all sub directories
		if [ $verbose -ne 0 ]; then echo "rm -rf ${temptargetdir}/*"; fi
		rm -rf "${temptargetdir}/"*
		fi
		if [ ! -d "${temptargetdir}" ]; then
			eval_gettext "Error ${program_name}: Couldn't create directory ${temptargetdir}"
			echo
			exit ${EX_SOFTWARE}
		fi
	done
	IFS="$ifs"
}

resolveTag () {
	# pessimistic aproach
	EXIT_CODE=${EX_SOFTWARE}

	tagname="$1"; shift
	tagdump="$1"; shift
	case "$tagname" in
	    genre) tagpattern="^GENRE";;
	    artist) tagpattern="^ARTIST";;
	    album) tagpattern="^ALBUM";;
	    year) tagpattern="^DATE";;
	    decade) tagpattern="^DATE";;
	esac
	tag="`echo \"$tagdump\" | grep \"$tagpattern\" | awk -F= '{ print $2 }'`"

	case "$tagname" in
		decade) if [ -n "$tag" ]; then tag="`perl -e \"print int($tag / 10) . 0 if $tag\"`"; fi
	esac

	# exchange '/', which is invalid for directory
	# names with the neutral character '-'
	tag="${tag//\//-}"

	# if the filetag begins with two dots '..'
	# than we exchange them with a '-', to avoid
	# confusing the file operations
	tag="${tag//#../-}"

	if [ -z "${tag}" ]; then
		tag="UNKNOWN $tagname"
	fi

	echo "$tag"
}

resolveTarget () {
	# pessimistic aproach
	EXIT_CODE=${EX_SOFTWARE}

	targetname="$1"; shift
	tagdump="$1"; shift

	for tag in $alltags; do
		if [ "$tag" = "$targetname" ]; then
			target="`resolveTag \"$targetname\" \"$tagdump\"`"
		fi
	done

	case "$targetname" in
	    artisttree) target="`resolveTag artist \"$tagdump\"`/`resolveTag album \"$tagdump\"`";;
	    genretree) target="`resolveTag genre \"$tagdump\"`/`resolveTag artist \"$tagdump\"`/`resolveTag album \"$tagdump\"`";;
	    yeartree) target="`resolveTag year \"$tagdump\"`/`resolveTag artist \"$tagdump\"`/`resolveTag album \"$tagdump\"`";;
	    decadetree) target="`resolveTag decade \"$tagdump\"`/`resolveTag artist \"$tagdump\"`/`resolveTag album \"$tagdump\"`";;
	esac

	echo "$target"
}

symlinksToFile () {

	# pessimistic aproach
	EXIT_CODE=${EX_SOFTWARE}

	file="$1"; shift
	filetype="$1"; shift
	filename="`basename "$file"`"

	case "$filetype" in
	    ogg)
		tagdump="`vorbiscomment -l "${file}"`"
		;;
	    mp3)
		tagdump="`mp3info -p 'GENRE=%g\nARTIST=%a\nALBUM=%n\nDATE=%y\n' \"${file}\" 2>/dev/null`"
		;;
	    *)
		echo "Error ${program_name}: unsupported filetype"
		echo
		exit ${EX_SOFTWARE}
		;;
	esac

	IFS="$newline"
	for targetname in $@; do IFS="$ifs";
		target="`resolveTarget "$targetname" "$tagdump"`"

		targettempdir="${tempdir}/_${targetname}"

		destdir="${targettempdir}/${target}"
		if [ ! -d "${destdir}" ]; then
			if [ $verbose -ne 0 ]; then eval_gettext "Creating ${destdir} ..."; echo; fi
			mkdir -p "${destdir}"
		fi
		if [ ! -d "${destdir}" ]; then
			eval_gettext "Warning ${program_name}: Couldn't create directory ${destdir}"
			echo
		else
			destfile="${destdir}/${filename}"
			if [ -e "${destfile}" ]; then
				eval_gettext "Warning ${program_name}: Link already exists. File '${filename}' is probably contained in more than one data directory!"
				echo
			else
				if [ $verbose -ne 0 ]; then eval_gettext "Linking ${file} to ${destfile}"; echo; fi
				ln -s "${file}" "${destfile}"
			fi
		fi
	done
	IFS="$ifs"
}

moveSymlinkDirs () {
	# pessimistic aproach
	EXIT_CODE=${EX_SOFTWARE}

	IFS="$newline"
	for targetname in $@; do IFS="$ifs";
		targettempdir="${tempdir}/_${targetname}"
		eval_gettext "Moving ${targettempdir} to ${basedir} ..."
		echo
		mv ${targettempdir} ${basedir} 2>/dev/null
		if [ ! -d "${basedir}/_${targetname}" ]; then
			eval_gettext "Error $program_name: Couldn't move ${targettempdir} to ${basedir}!"
			echo
			exit ${EX_SOFTWARE}
		fi
	done
	IFS="$ifs"
}

initTempTargetDirs $wantedtargets
find ${datadirs} \( -iname "*.mp3" -or -iname "*.mpeg" \) -and -type f -print | (while read file; do symlinksToFile "$file" mp3 $wantedtargets; done)
find ${datadirs} -iname "*.ogg" -and -type f -print | (while read file; do symlinksToFile "$file" ogg $wantedtargets; done)
moveSymlinkDirs $wantedtargets
rm -rf "${tempdir}"