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