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