### vim:ft=zsh:foldmethod=marker
### tagging functions for mp3, ogg vorbis as well as flac audio files
### these depend on id3v2, vorbiscomment and metaflac respectively.
### To import tags of a certain file into shell variables, ataglist.pl
### from arename >= v0.8 is needed.
###
### Frank Terbeck <ft@bewatermyfriend.org>
### Last-Modified: Sun Jul  6 13:07:38 2008
###
### URI: <http://ft.bewatermyfriend.org/comp/zsh.html>
###

emulate -L zsh
setopt extendedglob
setopt nullglob
setopt noksharrays

# variables {{{
local flactag mp3tag oggvtag taglist
local -A arguments
local -A mp3_opts
local -A oggv_opts
local -a oggv_args
local -a flac_list_opts
local -a flac_args
local file usage
local -i force dryrun

flactag='metaflac'
mp3tag='id3v2'
oggvtag='vorbiscomment'
if [[ -x $(which ataglist.pl) ]] ; then
    taglist='ataglist.pl'
else
    taglist='ataglist'
fi

flac_list_opts=(
    --no-utf8-convert
    --list --block-type="VORBIS_COMMENT"
)

flac_args=(
    --no-utf8-convert
)

mp3_opts=(
    album       "-A"
    artist      "-a"
    tracktitle  "-t"
    tracknumber "-T"
    year        "-y"
    compilation "--TPE2"
    genre       "--TCON"
)

oggv_opts=(     # used for flac, too.
    album       "ALBUM"
    artist      "ARTIST"
    tracktitle  "TITLE"
    tracknumber "TRACKNUMBER"
    year        "DATE"
    compilation "ALBUMARTIST"
    genre       "GENRE"
)

oggv_args=(-w)
#}}}
function available() { #{{{
    local com=$1 ; shift

    if [[ -x $(which ${com}) ]] ; then
        return 0
    fi

    printf 'Sorry, %s is not available. Cannot handle file.\n' "$com"
    return 1
}
#}}}
function flactag() { #{{{
    # except for listing, this one is pretty straight forward.
    local file tag

    file="$1"
    shift

    if (( ${#arguments} == 0 && force == 0 )) ; then
        (( dryrun == 0 )) && \
        print -l \
          ${${(M)${(f)"$(command $flactag $flac_list_opts $file)"}:#[ $'\t']##comment\[[0-9]##\]: *}/#[ $'\t']##comment\[[0-9]##\]: /}
        return 0
    fi

    for tag in ${(k)arguments} ; do
        flac_args+=("--remove-tag=${oggv_opts[$tag]}" "--set-tag=${oggv_opts[$tag]}=${arguments[$tag]}")
    done

    if (( force > 0 )) ; then
        print -- $flactag --remove-all-tags ${(qqq)file}
        (( dryrun == 0 )) && \
        command  $flactag --remove-all-tags $file
    fi
    if (( ${#flac_args} > 1 )) ; then
        print -- $flactag "${(@qqq)flac_args}" ${(qqq)file}
        (( dryrun == 0 )) && \
        command  $flactag "${(@)flac_args}" $file
    fi
}
#}}}
function listexterns() { #{{{
    cat << __EOF__
id3v2 - a command line id3v2 tag editor
    debian package: id3v2
    website: <http://id3v2.sourceforge.net/>

vorbiscomment - a command line ogg comment editor
    debian package: vorbis-tools
    website: <http://xiph.org/downloads/>

metaflac - a commandline flac comment editor
    debian package: flac
    website: <http://flac.sourceforge.net/download.html>

ataglist.pl
    debian package: -none-
    website: <http://ft.bewatermyfriend.org/comp/arename.html>
__EOF__
    return 0
}
#}}}
function mp3tag() { #{{{
    # This is pretty straight forward, as id3v2 overwrites given tags
    # by default and adds tags if the given tag isn't there yet.
    local -a options
    local key file

    file="$1"
    shift

    if (( ${#arguments} == 0 && force == 0 )) ; then
        command $mp3tag -R $file
        return 0
    fi

    for key in ${(k)arguments} ; do
        options+=( $mp3_opts[$key] "${arguments[$key]}" )
    done

    if (( force > 0 )) ; then
        print -- $mp3tag -D ${(qqq)file}
        (( dryrun == 0 )) && \
        command  $mp3tag -D $file
    fi
    if (( ${#options} > 0 )) ; then
        print -- $mp3tag -2 "${(@qqq)options}" ${(qqq)file}
        (( dryrun == 0 )) && \
        command  $mp3tag -2 "${(@)options}" $file
    fi
}
#}}}
function oggvtag() { #{{{
    # This one is a little more complicated, as we need to read
    # the currently set tags before being able to get the same
    # behaviour as id3v2.
    local -A tags
    local file tag val oifs
    local nil=$'\0'
    local nl=$'\n'

    file="$1"
    shift

    # no arguments? Just list the current tags of $file
    if (( ${#arguments} == 0 && force == 0 )) ; then
        (( dryrun == 0 )) && \
        command $oggvtag -l $file
        return 0
    fi

    tags=()
    if (( force == 0 )) ; then
        # reading the tags that are set in the given file.
        # save them to the $tags hash.
        oifs=$IFS
        IFS=$nil
        tags=(${="${(@)${(0)${${"$(command $oggvtag -l $file)"}/(#s)(#e)/empty=}//$nl/$nil}/(#b)([^=]##)=(*)/${(U)match[1]}$nil${match[2]}}"})
        IFS=$oifs
    fi

    # merging the already set tags with the command line arguments.
    for tag in ${(k)arguments} ; do
        val="${arguments[$tag]}"
        tags[${oggv_opts[$tag]}]="$val"
    done

    # 'FOO=' arguments should just remove the FOO tag
    for tag in ${(k)tags[(R)()]} ; do
        unset "tags[$tag]"
    done

    # assemble the arguments array for our tagging command.
    for tag in ${(k)tags} ; do
        oggv_args+=("-t" "$tag=${tags[$tag]}")
    done

    if (( force > 0 )) ; then
        print -- $oggvtag -w ${(qqq)file}
        (( dryrun == 0 )) && \
        : | command $oggvtag -w $file
    fi
    if (( ${#oggv_args} > 1 )) ; then
        print -- $oggvtag "${(@qqq)oggv_args}" ${(qqq)file}
        (( dryrun == 0 )) && \
        command  $oggvtag "${(@)oggv_args}" $file
    fi
}
#}}}
function showfeatures() { #{{{
    local com
    local -A features

    features=(
        ${mp3tag}
            'Reading and setting tags in mp3 files.'
        ${oggvtag}
            'Reading and setting tags in oggvorbis files.'
        ${flactag}
            'Reading and setting tags in flac files.'
        ${taglist}
            'Unified way to read tags in *all* filetypes.::This is only used for the '\''-e'\'' option.'
    )

    printf 'atag() requires a number of external programs to implement its features.\n'
    printf 'The following is a list of programs and features that depend on it:\n\n'

    for com in ${(k)features} ; do
        printf '    '${${features[$com]}//::/\\n    }'\n'
        printf '      checking for %s...\n' ${com}
        if [[ -x $(which ${com}) ]] ; then
            printf '        installed as %s; okay.\n\n' ${commands[$com]}
        else
            printf '        not found. Feature missing!\n\n'
        fi
    done

    return 0
}
#}}}
function tag_setenv() { #{{{
    local -A tags mapping
    local -a readtags
    local tag file=$1 ; shift

    mapping=(
        tracktitle  tt
        tracknumber tn
        album       al
        artist      ar
        year        yr
        compilation comp
        genre       gr
    )
    # hard reset
    for tag in ${(k)mapping} ; do typeset -g _${mapping[$tag]}='' ; done
    typeset -g _af=''

    readtags=(${(f)"$($taglist $file)"})
    if (( ( ${#readtags} % 2 ) == 0 )) ; then
        tags=(${readtags})
    else
        printf 'Reading tags failed. Fix them manually for this file (%s).\n' ${file}
        return 1
    fi

    typeset -g _af=$file
    for tag in ${(k)tags} ; do
        [[ -z ${mapping[$tag]} ]] && continue
        typeset -g _${mapping[$tag]}=${tags[$tag]}
    done
    if [[ -n ${tags[tracknumber]} ]] ; then
        typeset -g _tnp=${(l:2::0::0:)${${tags[tracknumber]}/\/*/}}
    else
        unset _tnp
    fi
#}}}
# function main() {{{

# usage text {{{
usage='atag [-F|-L] <file> [-d,-e,-f] <tag-definition(s)>
  Where <file> is either *.mp3, *.ogg or *.flac.
  The following set of common tags are supported:
    + al(bum)
    + ar(tist)
    + tn|tracknumber
    + tt|tracktitle
    + y(ear)
    + comp(ilation)
    + g(enre)

  Example:
    atag foo.ogg album='\''Bar Baz'\'' ar='\''Fearless Fo'\''

  If atag is called *only* with the '\''-F'\'' option, a list of
  features, that depend on external programs is printed, along with
  a test if the program is installed.
  Similarly, if '\''-L'\'' is the only option, you get a list, that
  provides information about where the required external programs
  can be found.

  If the '\''-f'\'' option is given, all tags will be removed, then
  the tags given on the command line will be added to the file.

  If the '\''-d'\'' options is given, the real commands are really run.

  if '\''-e'\'' is given, atag sets a few shell variables to the tag
  values in the given file. This *requires* the ataglist.pl script,
  included in arename.pl >= v0.8; This option overwrites all other
  options.

    the file name -> $_af
    tracknumber   -> $_tn
                     $_tnp
                        a postprocessed version of the tracknumber;
                        padded to two digits and the /Y portion of
                        x/Y removed. Thus 5 becomes 05, 10 stays 10
                        and 07/15 becomes 07.
    tracktitle    -> $_tt
    album         -> $_al
    artist        -> $_ar
    year          -> $_yr
    compilation   -> $_comp
    genre         -> $_gr

        Expample:
            %% atag foo.ogg -f ar="Zappa, Frank"
            %% atag foo.ogg -e
            %% atag foo.ogg atag foo.ogg \
                   ar=${_ar/(#b)(*), (*)/${match[2]} ${match[1]}}

    The example requires '\''extendedglob'\'' on and '\''ksharrays'\'' off.

' #}}}

(( setenv = 0 ))
(( force  = 0 ))
(( dryrun = 0 ))

case ${1} in
    (-F)
        showfeatures
        return 0
        ;;
    (-L)
        listexterns
        return 0
        ;;
esac

[[ -n $1 ]] && { file="${1}" ; shift ; }
while [[ $1 == -* ]] ; do
    case $1 in
        (-d)
            dryrun=1   ; shift;;
        (-e)
            setenv=1   ; shift;;
        (-f)
            force=1    ; shift;;
        (*)
            printf "unknown option %s\n\n" "$1"
            printf "${usage}"
            return 1;;
    esac
done

if (( setenv > 0 )) ; then
    available $taglist || return 1
    tag_setenv ${file}
    return $?
fi

while [[ -n "$1" ]] ; do
    case $1 in
        ((#b)ar(|tist)=(*))
            arguments[artist]="$match[2]"       ;;
        ((#b)al(|bum)=(*))
            arguments[album]="$match[2]"        ;;
        ((#b)(tt|tracktitle)=(*))
            arguments[tracktitle]="$match[2]"   ;;
        ((#b)(tn|tracknumber)=(*))
            arguments[tracknumber]="$match[2]"  ;;
        ((#b)y(|ear)=(*))
            arguments[year]="$match[2]"         ;;
        ((#b)comp(|ilation)=(*))
            arguments[compilation]="$match[2]"  ;;
        ((#b)g(|enre)=(*))
            arguments[genre]="$match[2]"  ;;
        (*)
            printf 'unknown argument "%s"\n' "$1"
            return 1
            ;;
    esac
    shift
done

if [[ -n ${file} ]] && [[ ! -e ${file} ]] ; then
    printf 'Cannot open "%s": file does not exist.\n' "$file"
    return 1
fi

case ${file} in
    ((#i)*.ogg)
        available $oggvtag || return 1
        oggvtag "$file"
        ;;
    ((#i)*.mp3)
        available $mp3tag  || return 1
        mp3tag "$file"
        ;;
    ((#i)*.flac)
        available $flactag || return 1
        flactag "$file"
        ;;
    (*)
        [[ -n ${file} ]] && printf 'Unknown filetype (%s)\n\n' "${file}"
        printf "${usage}"
        ;;
esac
#}}}