#!/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, 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

# 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

# variables
verbose=0
casesensitive=0
organize_artists=0
organize_comments=0
organize_genre=0
organize_initials=0
organize_years=0
organize_albums=0

# 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 a virtual directory structure."
	echo
	gettext "Options:"
	echo
	gettext " --all              organize on the basis of all the tags"
	echo
	gettext " -A|--artist        organize on the basis of the artist tag"
	echo
	gettext " -C|--comments      organize on the basis of the the comments tag"
	echo
	gettext " -G|--genre         organize on the basis of the genre tag"
	echo
	gettext " -I|--initials      organize on the basis of the initial letters"
	echo
	gettext " -Y|--year          organize on the basis of the year tag"
	echo
	gettext " -L|--album         organize on the basis of the album tag"
	echo
	gettext " -h|--help          print this help screen"
	echo
	gettext " -s|--casesensitive case sensitive lookup of initials"
	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 ()
{
	program_name=$(basename $0)
	eval_gettext "Usage: $program_name [options] basedir"
	echo
	print_help
}

TEMP=$(getopt -o :ACGIYLhsv --long all,artist,comments,genre,initials,year,album,help,casesensitive,verbose,version -n "$(basename $0)" -- "$@")

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

eval set -- "$TEMP"

while true ; do
	case "$1" in
		--all) organize_artists=1
		       organize_comments=1
		       organize_genre=1
		       organize_initials=1
		       organize_years=1
		       organize_albums=1
		       shift;;
		-A|--artist) organize_artists=1; shift;;
		-C|--comments) organize_comments=1; shift;;
		-G|--genre) organize_genre=1; shift;;
		-I|--initials) organize_initials=1; shift;;
		-Y|--year) organize_years=1; shift;;
		-L|--album) organize_albums=1; shift;;
		-h|--help) print_help ; exit ${EX_OK} ; shift;;
		-s|--casesensitive) casesensitive=1; 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
basedir=${1}
tempdir="/tmp"
if [ "${basedir:0:1}" != "/" ]; then
    basedir="`pwd`/${basedir}"
fi

if [ ! -d ${basedir} ]; then
	program_name=$(basename $0)
    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
		program_name=$(basename $0)
		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

# read in the data
datacontent="`find ${datadirs} \( -name "*.mp3" -or -name "*.ogg" \) -and -type f -print 2>/dev/null`"

# organize on the basis of the initial letter
if [ $organize_initials -ne 0 ]; then
    initialdir="${tempdir}/_initial"
    if [ ! -d ${initialdir} ]; then
		mkdir ${initialdir}
    else
        # remove all subdirectories
		if [ $verbose -ne 0 ]; then echo "rm -rf ${initialdir}/*"; fi
		rm -rf ${initialdir}/*
    fi
    if [ ! -d ${initialdir} ]; then
		eval_gettext "Error `basename $0`: Couldn't create directory ${initialdir}!"
		echo
		exit ${EX_SOFTWARE}
    else
        # set separator character to a newline
		IFS='
'
		for file in ${datacontent}; do
			filename=`basename ${file}`
			if [ ${casesensitive} -ne 0 ]; then
				fileinitial="${filename:0:1}"
			else
				fileinitial="`echo ${filename:0:1} | tr [:lower:] [:upper:]`"
			fi
			destdir="${initialdir}/${fileinitial}"
			if [ ! -d ${destdir} ]; then
				if [ $verbose -ne 0 ]; then eval_gettext "Creating ${destdir} ..." ; echo ; fi
				mkdir ${destdir}
			fi
			if [ ! -d ${destdir} ]; then
				program_name=$(basename $0)
				eval_gettext "Warning ${program_name}: Couldn't create directory ${destdir}!"
				echo
			else
				destfile="${destdir}/${filename}"
				if [ -e "${destfile}" ]; then
					program_name=$(basename $0)
					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
    fi
    eval_gettext "Moving ${initialdir} to ${basedir} ..."
    echo
    mv "${initialdir}" "${basedir}" 2>/dev/null
	if [ ! -d "${basedir}/_initial" ]; then
		eval_gettext "Error `basename $0`: Couldn't move ${initialdir} to ${basedir}!"
		echo
		exit ${EX_SOFTWARE}
	fi
fi

#
# Function, which builds a directory structure consiting of symbolic
# links to the corresponding files in the _data directory.
# To determine the association the mp3info utility with the given
# tagpattern gets used.
#
# @param tagname name of ID3 tag on base of which to organize the links
#        i.e.: genre
# @param tagpattern pattern for mp3info
#        i.e.  %g\n
# @return Sets EXIT_CODE to EX_OK on success!
#
organizeOnBaseOfTag()
{
    local tagname=${1}
    local tagpattern=${2}

    # pessimistic aproach
    EXIT_CODE=${EX_SOFTWARE}

    if [[ -n "${tagname}" && -n "$tagpattern" ]]; then

		tagdir="${tempdir}/_${tagname}"
		if [ ! -d ${tagdir} ]; then
			mkdir ${tagdir}
		else
            # remove all sub directories
			if [ $verbose -ne 0 ]; then echo "rm -rf ${tagdir}/*"; fi
			rm -rf ${tagdir}/*
		fi
		if [ ! -d ${tagdir} ]; then
			program_name=$(basename $0)
			eval_gettext "Error ${program_name}: Couldn't create directory ${tagdir}"
			echo
			EXIT_CODE=${EX_SOFTWARE}
		else
            # set separator character to a newline
			IFS='
'
			for file in ${datacontent}; do
				filename=`basename ${file}`
				tag="`mp3info -p \"${tagpattern}\" ${file} 2>/dev/null`"
                    # 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"
				fi
					
				destdir="${tagdir}/${tag}"
				if [ ! -d ${destdir} ]; then
					if [ $verbose -ne 0 ]; then eval_gettext "Creating ${destdir} ..."; echo; fi
					mkdir ${destdir}
				fi
				if [ ! -d ${destdir} ]; then
					program_name=$(basename $0)
					eval_gettext "Warning ${program_name}: Couldn't create directory ${destdir}"
					echo
				else
					destfile="${destdir}/${filename}"
					if [ -e "${destfile}" ]; then
						program_name=$(basename $0)
						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

	    # success
			EXIT_CODE=${EX_OK}	

		fi
    eval_gettext "Moving ${tagdir} to ${basedir} ..."
    echo
    mv ${tagdir} ${basedir} 2>/dev/null
		if [ ! -d "${basedir}/_${tagname}" ]; then
	                eval_gettext "Error `basename $0`: Couldn't move ${tagdir} to ${basedir}!"
			echo
			exit ${EX_SOFTWARE}
		fi
    else
		program_name=$(basename $0)
		eval_gettext "Error ${program_name}: missing parameter: tagname = ${tagname}, tagpattern = ${tagpattern}"
		echo
		EXIT_CODE=${EX_SOFTWARE}
    fi
}

if [ ${organize_genre} -ne 0 ]; then
    organizeOnBaseOfTag "genre" "%g\n"
fi

if [ ${organize_artists} -ne 0 ]; then
    organizeOnBaseOfTag "artist" "%a\n"
fi

if [ ${organize_albums} -ne 0 ]; then
    organizeOnBaseOfTag "album" "%l\n"
fi

if [ ${organize_years} -ne 0 ]; then
    organizeOnBaseOfTag "year" "%y\n"
fi

if [ ${organize_comments} -ne 0 ]; then
    organizeOnBaseOfTag "comments" "%c\n"
fi
