#!/bin/bash #----------------------------------------------------------------------------------------------------------------------------------- # # Music Tools Library # # Copyright (C) 2016-2026 Arnaud G. GIBERT # mailto:arnaud@rx3.net # # This is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published # by the Free Software Foundation, either version 3 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; If not, see # . # #----------------------------------------------------------------------------------------------------------------------------------- if [[ "${MUSIC_TOOLS_LIB}" != "" ]] then return else declare -g MUSIC_TOOLS_LIB=1 fi #----------------------------------------------------------------------------------------------------------------------------------- # Includes #----------------------------------------------------------------------------------------------------------------------------------- : "${RX3_LIB_DIR:=/usr/lib/rx3}" . "${RX3_LIB_DIR}/base.bash" #----------------------------------------------------------------------------------------------------------------------------------- # Global Variable #----------------------------------------------------------------------------------------------------------------------------------- declare -Ag MT_FLAC_2_MP3_TAB=( [TRACKNUMBER]=TRCK [TRACKTOTAL]=TRCK [TITLE]=TIT2 [ALBUM]=TALB [ARTIST]=TPE1 [COMPOSER]=TCOM [ARRANGER]=IPLS [CONDUCTOR]=TPE3 [REMIXER]=TPE4 [AUTHOR]=TEXT [WRITER]=TEXT [LYRICIST]=TEXT [ALBUMARTIST]=TPE2 [DATE]=TDRC [GENRE]=TCON [COMPILATION]=TCMP [DISCTOTAL]=TPOS [DISCNUMBER]=TPOS [LABEL]=XXXX [PUBLISHER]=TPUB [ISRC]=TSRC [BARCODE]=TXXX [CATALOGNUMBER]=TXXX [COPYRIGHT]=TCOP [LICENSE]=USER [COMMENT]=COMM [DESCRIPTION]=TIT3 [ENCODER]=TSSE [MEDIA]=TMED [LANGUAGE]=TLAN [LYRICS]=USLT ) declare -Ag MT_MP3_2_FLAC_TAB=( [TRCK]=TRACKNUMBER:TRACKTOTAL [TIT2]=TITLE [TALB]=ALBUM [TPE1]=ARTIST [TCOM]=COMPOSER [IPLS]=ARRANGER [TPE3]=CONDUCTOR [TPE4]=REMIXER [TEXT]=AUTHOR:WRITER:LYRICIST [TPE2]=ALBUMARTIST [TDRC]=DATE [TCON]=GENRE [TCMP]=COMPILATION [TPOS]=DISCNUMBER:DISCTOTAL [TPUB]=PUBLISHER [TSRC]=ISRC [TXXX]=BARCODE:CATALOGNUMBER [TCOP]=COPYRIGHT [USER]=LICENSE [COMM]=COMMENT [TIT3]=DESCRIPTION [TSSE]=ENCODER [TMED]=MEDIA [TLAN]=LANGUAGE [USLT]=LYRICS ) declare -g MT_FLAC_TAG_LIST="TRACKNUMBER TRACKTOTAL TITLE ALBUM ARTIST COMPOSER ARRANGER CONDUCTOR REMIXER AUTHOR WRITER LYRICIST ALBUMARTIST DATE GENRE COMPILATION LABEL PUBLISHER ISRC BARCODE UPC COPYRIGHT LICENSE COMMENT DESCRIPTION ENCODER MEDIA LANGUAGE LYRICS" declare -g MT_FLAC_TAG_COND=$(echo '('${MT_FLAC_TAG_LIST}')' | sed -e 's/ /|/g') declare -g MT_NODEF_TAG_LIST="TRACKNUMBER TITLE ISRC LYRICS" declare -g MT_NODEF_TAG_COND=$(echo '('${MT_NODEF_TAG_LIST}')' | sed -e 's/ /|/g') #----------------------------------------------------------------------------------------------------------------------------------- # Fix Name #----------------------------------------------------------------------------------------------------------------------------------- # Standardise Name #----------------------------------------------------------------------------------------------------------------------------------- # $1: Input Name # $2: SED Substitute Patern # Out: Output Name #----------------------------------------------------------------------------------------------------------------------------------- mt_fix_name() { local input_name="$1" local ssp="$2" if [[ "${ssp}" != "" ]] then sse="s/${ssp}" else sse="s/^//" fi echo ${input_name} | sed -e 's/[ ,!/?:]/_/g' \ -e "s/['’´]/_/g" \ -e 's/\[/-/g' -e 's/\]//g' \ -e 's/(/-/g' -e 's/)//g' \ -e 's/__/_/g' -e 's/_\././g' -e 's/_-/-/g' -e 's/--/-/g' -e 's/-_/-/g' \ -e 's/à/a/g' -e 's/á/a/g' -e 's/ä/a/g' -e 's/â/a/g' -e 's/ã/a/g' -e 's/é/e/g' -e 's/è/e/g' -e 's/ê/e/g' -e 's/ë/e/g' -e 's/ï/i/g' -e 's/î/i/g' -e 's/í/i/g' -e 's/ô/o/g' -e 's/ö/o/g' -e 's/ó/o/g' -e 's/ù/u/g' -e 's/ü/u/g' -e 's/û/u/g' -e 's/ú/u/g' -e 's/ç/c/g' -e 's/ñ/n/g' \ -e 's/À/a/g' -e 's/Á/a/g' -e 's/Ä/a/g' -e 's/Â/a/g' -e 's/Ã/a/g' -e 's/É/e/g' -e 's/È/e/g' -e 's/Ê/e/g' -e 's/Ë/e/g' -e 's/Ï/i/g' -e 's/Î/i/g' -e 's/Í/i/g' -e 's/Ô/o/g' -e 's/Ö/o/g' -e 's/Ó/o/g' -e 's/Ù/u/g' -e 's/Ü/u/g' -e 's/Û/u/g' -e 's/Ú/u/g' -e 's/Ç/c/g' -e 's/Ñ/n/g' \ -e 's/¿//g' \ -e 's/[+&]/and/g' -e 's/"/_inch/g' \ -e 's/#//g' \ -e 's/^[_-]*//' \ -e 's/[_-]*$//' \ -e "${sse}" | tr '[:upper:]' '[:lower:]' } #----------------------------------------------------------------------------------------------------------------------------------- # Fix File Name #----------------------------------------------------------------------------------------------------------------------------------- # Standardise File Name #----------------------------------------------------------------------------------------------------------------------------------- # $1: Input File Name # $2: SED Substitute Patern # Out: Output File Name #----------------------------------------------------------------------------------------------------------------------------------- mt_fix_file_name() { local input_file_name="$1" local ssp="$2" mt_fix_name "${input_file_name}" "${ssp}" | sed -e 's/\(^[0-9]*\)./\1-/' } #----------------------------------------------------------------------------------------------------------------------------------- # TagTab Alloc #----------------------------------------------------------------------------------------------------------------------------------- mt_tagtab_alloc() { unset TAGTAB declare -Ag TAGTAB declare -g TAGLIST TAGLIST=":" } #----------------------------------------------------------------------------------------------------------------------------------- # Tag Exist #----------------------------------------------------------------------------------------------------------------------------------- mt_tag_exist() { local tag="$1" echo ${TAGLIST} | grep -e ":${tag}:" } #----------------------------------------------------------------------------------------------------------------------------------- # TagTab Read #----------------------------------------------------------------------------------------------------------------------------------- mt_tagtab_read() { local track_id="$1" local track_file="$2" local mode="$3" tmp_file=$(mktemp) case "${track_file}" in *.flac) metaflac --export-tags-to=- "${track_file}" | sed -e 's/^[^=]*=/\U&\E/' > ${tmp_file} while read line do tag="${line/=*}" if [[ ${line/*=*/=} == "=" ]] && [[ "${tag}" != "" ]] then value="$(mt_tag_read "${track_file}" "${tag}")" if [[ "$(mt_tag_exist ${tag})" == "" ]] then TAGLIST="${TAGLIST}${tag}:" fi if [[ ( "${mode}" != "FACTORING" ) || ( "${tag}" =~ ^${MT_NODEF_TAG_COND}$ ) || ( -v "TAGTAB[\"0,${tag}\"]}" ) ]] then if [[ ( ! -v "TAGTAB[\"0,${tag}\"]}" ) || ( "${TAGTAB["0,${tag}"]}" != "${value}") ]] then TAGTAB["${track_id},${tag}"]="$value" fi else TAGTAB["0,${tag}"]="${value}" fi fi done < ${tmp_file} ;; *.mp3) mid3v2 --list "${track_file}" | tail -n +2 | sed -e 's/^[^=]*=/\U&\E/' > ${tmp_file} while read line do mp3tag="${line/=*}" value="$(mt_tag_read "${track_file}" "${mp3tag}")" if [[ "${line/*=*/=}" == "=" ]] && [[ "${mp3tag}" != "" ]] && [[ "${MT_MP3_2_FLAC_TAB["${mp3tag}"]}" != "" ]] then tag="${MT_MP3_2_FLAC_TAB["${mp3tag}"]}" if [[ "$(mt_tag_exist ${tag})" == "" ]] then TAGLIST="${TAGLIST}${tag}:" fi if [[ ( "${mode}" != "FACTORING" ) || ( "${tag}" =~ ^${MT_NODEF_TAG_COND}$ ) || ( "${TAGTAB["0,${tag}"]}" != "" ) ]] then if [[ "${TAGTAB["0,${tag}"]}" != "${value}" ]] then TAGTAB["${track_id},${tag}"]="$value" fi else TAGTAB["0,${tag}"]="${value}" fi fi done < ${tmp_file} ;; *) echo_error "mt_tagtab_read: Unknown file format: ${track_file}" ;; esac if [[ ${TAGLIST} == ":" ]] then TAGLIST="" fi \rm -f ${tmp_file} } #----------------------------------------------------------------------------------------------------------------------------------- # TagTab Load #----------------------------------------------------------------------------------------------------------------------------------- mt_tagtab_load() { local metadata_file="$1" local mode="$2" track_id=0 eoa="false" while read -r line && [[ "${eoa}" != "true" ]] do tag="${line/=*}" value="${line/${tag}=}" case "${tag}" in @${MT_FLAC_TAG_COND}) TAGTAB["${track_id},${tag}"]="$value" ;; DEF) ;; SOT) track_id=$((${track_id} + 1)) ;; EOT) ;; EOA) eoa="true" ;; *) echo_error "mt_tagtab_load: Unkwown tag: '${tag}'" ;; esac done < "${metadata_file}" } #----------------------------------------------------------------------------------------------------------------------------------- # TagTab Full Dump #----------------------------------------------------------------------------------------------------------------------------------- mt_tagtab_full_dump() { echo "--- TAGTAB Dump ---" for key in "${!TAGTAB[@]}" do printf "TAGTAB[%q]=\"%s\"\n" "$key" "${TAGTAB[$key]}" done echo "--- End Dump ---" } #----------------------------------------------------------------------------------------------------------------------------------- # TagTab Dump #----------------------------------------------------------------------------------------------------------------------------------- mt_tagtab_dump() { local track_id="$1" local dump_mode="$2" case "${dump_mode}" in "STANDARD") dump_list="${MT_FLAC_TAG_LIST}" ;; "PASSTHROUGH") dump_list="${TAGLIST//:/ }" ;; "CUSTOM") dump_list="$3" ;; esac for tag in ${dump_list} do if [[ -v "TAGTAB[\"${track_id},${tag}\"]" ]] then echo "${tag}=${TAGTAB["${track_id},${tag}"]}" fi done } #----------------------------------------------------------------------------------------------------------------------------------- # Tag Get #----------------------------------------------------------------------------------------------------------------------------------- mt_tag_get() { local declare -n return=$1 local track_id=$2 local tag=$3 return="${TAGTAB["${track_id},${tag}"]}" if [[ "${return}" == "" ]] then return="${TAGTAB["0,${tag}"]}" if [[ "${return}" == "" ]] && [[ "${tag}" == "TRACKNUMBER" ]] then return="$(printf "%02d" ${track_id})" fi fi } #----------------------------------------------------------------------------------------------------------------------------------- # Tag Save #----------------------------------------------------------------------------------------------------------------------------------- mt_tag_save() { local target_file=$1 local track_id=$2 local mode=$3 if [[ "${mode}" == "CLEAN" ]] then mt_tag_all_delete "${target_file}" "" fi for tag in ${MT_FLAC_TAG_LIST} do # if [[ "${TAGTAB["${track_id},${tag}"]}" == "" ]] # then # value="${TAGTAB["0,${tag}"]}" # # if [[ "${value}" == "" ]] && [[ "${tag}" == "TRACKNUMBER" ]] # then # value="$(printf "%02d" ${track_id})" # fi # else # value="${TAGTAB["${track_id},${tag}"]}" # fi mt_tag_get value "${track_id}" "${tag}" mt_tag_delete "${target_file}" "${tag}" if [[ "${value}" != "" ]] then mt_tag_write "${target_file}" "${tag}" "${value}" fi done } #----------------------------------------------------------------------------------------------------------------------------------- # Tag Read #----------------------------------------------------------------------------------------------------------------------------------- mt_tag_read() { local track_file="$1" local tag="$2" case "${track_file}" in *.flac) sh_exec "metaflac --show-tag=\"${tag}\" \"${track_file}\" | sed -e 's/^[^=]*=//' -e ':a' -e 'N' -e '"'$!'"ba' -e 's/\n/\\n/g'" ;; *.mp3) # sh_exec "mid3v2 --list \"${track_file}\" | tail -n +2 | sed -e 's/^[^=]*=/\U&\E/' | grep -e \"^${MT_FLAC_2_MP3_TAB[\"${tag}\"]}\" | sed -e 's/^[^=]*=//' -e ':a' -e 'N' -e '"'$!'"ba' -e 's/\n/\\n/g'" sh_exec "mid3v2 --list \"${track_file}\" | tail -n +2 | grep -e \"^${tag}\" | sed -e 's/^[^=]*=//' -e ':a' -e 'N' -e '"'$!'"ba' -e 's/\n/\\n/g'" ;; *) echo_error "mt_tag_read: Unknown file format: ${track_file}" ;; esac } #----------------------------------------------------------------------------------------------------------------------------------- # Tag Write #----------------------------------------------------------------------------------------------------------------------------------- mt_tag_write() { local track_file="$1" local tag="$2" local value="$3" value="$(echo -e "$(str_escape "${value}")")" case "${track_file}" in *.flac) cmd_exec metaflac --set-tag="${tag}=${value}" "${track_file}" ;; *.mp3) cmd_exec mid3v2 --"${MT_FLAC_2_MP3_TAB["${tag}"]}" "${value}" "${track_file}" ;; *) echo_error "mt_tag_write: Unknown file format: ${track_file}" ;; esac } #----------------------------------------------------------------------------------------------------------------------------------- # Tag Delete #----------------------------------------------------------------------------------------------------------------------------------- mt_tag_delete() { local track_file="$1" local tag="$2" case "${track_file}" in *.flac) cmd_exec metaflac --remove-tag="${tag}" "${track_file}" ;; *.mp3) cmd_exec mid3v2 --delete-frames="${MT_FLAC_2_MP3_TAB["${tag}"]}" "${track_file}" ;; *) echo_error "mt_tag_delete: Unknown file format: ${track_file}" ;; esac } #----------------------------------------------------------------------------------------------------------------------------------- # Tag All Delete #----------------------------------------------------------------------------------------------------------------------------------- mt_tag_all_delete() { local track_file="$1" local header_type="$2" if [[ "${header_type}" == "" ]] then case "${track_file##*.}" in flac) header_type="ogg" ;; mp3) header_type="id3" ;; esac fi if [[ ( "${header_type}" == "ogg") && ( "${track_file##*.}" == "mp3") ]] then echo_error "mt_tag_all_delete: Can't remove ogg tags from mp3 file!" else case "${header_type}" in ogg) cmd_exec metaflac --remove-all-tags "${track_file}" ;; id3) cmd_exec mid3v2 --delete-all "${track_file}" ;; *) echo_error "mt_tag_all_delete: Unknown header type: ${header_type}" ;; esac fi } #----------------------------------------------------------------------------------------------------------------------------------- # Album Type Get #----------------------------------------------------------------------------------------------------------------------------------- mt_album_type_get() { local track_file="$1" for type in various_artists tribute original_soundtrack child do if [[ ${track_file} == */${type}/* ]] then echo "${type}" fi done }