diff options
-rwxr-xr-x | localsonganizer | 433 |
1 files changed, 433 insertions, 0 deletions
diff --git a/localsonganizer b/localsonganizer new file mode 100755 index 0000000..6836118 --- /dev/null +++ b/localsonganizer @@ -0,0 +1,433 @@ +#!/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 |