#!/bin/bash
#
# songanizer - script to organize ogg and mp3 files
# Copyright (c) 2002-2003 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 tries 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 (like pointers) to the directories in which
# the real files live.
# 
# The goal is to create virtual directory structures, which give different
# views of the data, but without having redundant copies of the files themself.
#
# dependences:
#  - It's a bash script and therefore needs a running bash.
#  - 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 which name starts 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 base of the artist tag
#   _genre    contains the link structure on the base of the genre tag
#   _initial  contains the link structure on the base of the initials
#   ... and more, depending on the switchs passed to the script
#
#
# @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:
# - an organization containing the last 100 created files. To check ID3 tags.
# - change creation algo, so it will first create the structrue in a tmp
#   directory and only after completion of this heavy task, in a short
#   operation exchange the old structure with the newly created
# - add support for ID3v2 Tags - probably should go into mp3info
#

# 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_initial=0
organize_years=0
organize_albums=0

# interpretation of passed options
while getopts ":ACGIYLhsv" Option
  do
  case ${Option} in
	  A)
		  organize_artists=1
		  ;;
	  C)
		  organize_comments=1
		  ;;
      G)
		  organize_genre=1
		  ;;
      I)
          organize_initial=1
          ;;
      Y)
		  organize_years=1
		  ;;
      L)
		  organize_albums=1
		  ;;
      h)
		  echo "Organizes files in a virtual directory structure."
		  echo
		  echo "Options:"
		  echo "A -> organize on base of artist tag"
		  echo "C -> organize on base of comment tag"
		  echo "G -> organize on base of genre tag"
		  echo "I -> organize on base of initial letters"
		  echo "Y -> organize on base of year tag"
		  echo "L -> organize on base of album tag"
		  echo "h -> prints this help screen"
		  echo "s -> case sensitive on initials lookup"
		  echo "v -> activate verbose mode"
		  exit ${EX_OK}
		  ;;
      s)
		  casesensitive=1
		  ;;
      v)
		  verbose=1
		  ;;
      *)
		  echo "Unimplemented option choosen. Use -h to visualize a help screen."
		  exit ${EX_USAGE}
		  ;;
  esac
done
shift $(($OPTIND - 1))

if [ $# -eq 0 ]; then
    echo "Usage: `basename $0` [options] basedir"
    exit ${EX_USAGE}
fi
basedir=${1}
if [ "${basedir:0:1}" != "/" ]; then
    basedir="`pwd`/${basedir}"
fi

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

datadir="${basedir}/_data"

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

# read in the data
datacontent="`ls --quoting-style=literal -d ${datadir}*/* 2>/dev/null`"

# organize on the basis of the initial letter
if [ $organize_initial -ne 0 ]; then
    initialdir="${basedir}/_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
		echo "Error `basename $0`: Couldn't create directory ${initialdir}"
		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 echo "Creating ${destdir} ..."; fi
				mkdir ${destdir}
			fi
			if [ ! -d ${destdir} ]; then
				echo "Warning `basename $0`: Couldn't create directory ${destdir}"
			else
				destfile="${destdir}/${filename}"
				if [ -e "${destfile}" ]; then
					echo "Warning `basename $0`: Link already exists. Album '${filename}' is probably contained in more than one data directory!"
				else
					if [ $verbose -ne 0 ]; then echo "Linking ${file} to ${destfile}"; fi
					ln -s "${file}" "${destfile}"
				fi
			fi
		done
    fi
fi

#
# Function, which builds a directory structure consiting of symbolic
# links to the corrispondig directories 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="${basedir}/_${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
			echo "Error `basename $0`: Couldn't create directory ${tagdir}"
			EXIT_CODE=${EX_SOFTWARE}
		else
            # set separator character to a newline
			IFS='
'
			for file in ${datacontent}; do
				filename=`basename ${file}`

				songlist="`ls --quoting-style=literal -d ${file}/* 2>/dev/null`"
				
				tag=
				for song in ${songlist}; do
					filetag="`mp3info -p \"${tagpattern}\" ${song} 2>/dev/null`"
                    # exchange '/', which is invalid for directory
                    # names with the neutral character '-'
					filetag=${filetag//\//-}

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

						if [ -n "${filetag}" ]; then
							tag=${filetag}
							break;
						fi
				done
				
				if [ -z "${tag}" ]; then
					tag="UNKNOWN"
				fi
				
				destdir="${tagdir}/${tag}"
				if [ ! -d ${destdir} ]; then
					if [ $verbose -ne 0 ]; then echo "Creating ${destdir} ..."; fi
					mkdir ${destdir}
				fi
				if [ ! -d ${destdir} ]; then
					echo "Warning `basename $0`: Couldn't create directory ${destdir}"
				else
					destfile="${destdir}/${filename}"
					if [ -e "${destfile}" ]; then
						echo "Warning `basename $0`: Link already exists. Album '${filename}' is probably contained in more than one data directory!"
					else
						if [ $verbose -ne 0 ]; then echo "Linking ${file} to ${destfile}"; fi
						ln -s "${file}" "${destfile}"
					fi
				fi
			done

	    # success
			EXIT_CODE=${EX_OK}	

		fi
    else
		echo "Error `basename $0`: missing parameter: tagname = ${tagname}, tagpattern = ${tagpattern}"
		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


