Compare commits

..

17 Commits

3 changed files with 256 additions and 285 deletions

249
cuetag.sh
View File

@@ -1,249 +0,0 @@
#! /bin/bash
# cuetag.sh - tag files based on cue/toc file information
# uses cueprint output
# usage: cuetag.sh <cuefile|tocfile> [file]...
CUEPRINT=cueprint
cue_file=""
function check_required {
local err=
for var in ${@}; do
if eval "[[ -z \$$var ]]"; then
>&2 echo "CUETAG: Tag $var required but zero"
err=1
fi
done
if [[ -n $err ]]; then
exit 1
fi
}
# print usage instructions
usage()
{
echo "usage: cuetag.sh <cuefile|tocfile> [file]..."
echo
echo "cuetag.sh adds tags to files based on cue or toc information"
echo
echo "Supported formats (format extension, format name, tagging utility):"
echo "ogg, Ogg Vorbis, vorbiscomment"
echo "flac, FLAC, metaflac"
echo "mp3, MP3, mp3info"
echo "txt, Vorbis Comment Text File, tee"
echo
echo "cuetag.sh uses cueprint, which must be in your path"
}
# Vorbis Comments
# for FLAC and Ogg Vorbis files
vorbis()
{
trackno=$1; shift
file="$1"; shift
fields="$@"
# FLAC tagging
# --remove-all-tags overwrites existing comments
METAFLAC="metaflac --remove-all-tags --import-tags-from=-"
# Ogg Vorbis tagging
# -w overwrites existing comments
# -a appends to existing comments
VORBISCOMMENT="vorbiscomment -w -c -"
# VC text file format
# TODO: this also outputs to stdout
TXTFILE="tee"
case "$file" in
*.[Ff][Ll][Aa][Cc])
VORBISTAG=$METAFLAC
;;
*.[Oo][Gg][Gg])
VORBISTAG=$VORBISCOMMENT
;;
*.[Tt][Xx][Tt])
VORBISTAG=$TXTFILE
;;
esac
# space separated list of recommended standard field names
# see http://www.xiph.org/ogg/vorbis/doc/v-comment.html
# TRACKTOTAL is not in the Xiph recommendation, but is in common use
[ -n "$fields" ] ||
fields='TITLE VERSION ALBUM TRACKNUMBER TRACKTOTAL ARTIST PERFORMER COPYRIGHT LICENSE ORGANIZATION DESCRIPTION GENRE DATE LOCATION CONTACT ISRC'
# fields' corresponding cueprint conversion characters
# separate alternates with a space
TITLE='%t'
VERSION=''
ALBUM='%T'
TRACKNUMBER='%02n'
TRACKTOTAL='%02N'
ARTIST='%c %p'
PERFORMER='%p'
COPYRIGHT=''
LICENSE=''
ORGANIZATION=''
DESCRIPTION='%m'
# --------------------------------- my hack! ---------------------------------
#GENRE='%g'
#DATE=''
DATE=`sed -n 's!^REM DATE "\?\([^"]*\)"\?!\1!p' "$cue_file" | tr -d '[:space:]'`
GENRE=`sed -n 's!^REM GENRE "\?\([^"]*\)"\?!\1!p' "$cue_file" | tr -d '[:space:]'`
# --------------------------------- my hack! ---------------------------------
LOCATION=''
CONTACT=''
ISRC='%i %u'
check_required ARTIST DATE ALBUM TRACKNUMBER TITLE
(for field in $fields; do
case "$field" in
(*=*) echo "$field";;
(*)
value=""
for conv in $(eval echo \$$field); do
# --------------------------------- my hack! ---------------------------------
case $field in
DATE) value=$DATE
;;
GENRE) value=$GENRE
;;
*) value=$($CUEPRINT -n $trackno -t "$conv\n" "$cue_file")
;;
esac
# --------------------------------- my hack! ---------------------------------
if [ -n "$value" ]; then
echo "$field=$value"
break
fi
done
;;
esac
done) | $VORBISTAG "$file"
}
id3()
{
MP3TAG=$(which mid3v2) \
|| MP3TAG=$(which id3v2)
if [ -z "${MP3TAG}" ]; then
echo "error: not found '(m)id3v2'."
exit 1
fi
# space separated list of ID3 v1.1 tags
# see http://id3lib.sourceforge.net/id3/idev1.html
fields="TITLE ALBUM ARTIST YEAR COMMENT GENRE TRACKNUMBER"
# fields' corresponding cueprint conversion characters
# separate alternates with a space
TITLE='%t'
ALBUM='%T'
ARTIST='%p'
YEAR=''
COMMENT='%c'
GENRE='%g'
TRACKNUMBER='%n'
for field in $fields; do
case "$field" in
*=*) value="${field#*=}";;
*)
value=""
for conv in $(eval echo \$$field); do
value=$($CUEPRINT -n $1 -t "$conv\n" "$cue_file")
if [ -n "$value" ]; then
break
fi
done
;;
esac
if [ -n "$value" ]; then
case $field in
TITLE)
$MP3TAG -t "$value" "$2"
;;
ALBUM)
$MP3TAG -A "$value" "$2"
;;
ARTIST)
$MP3TAG -a "$value" "$2"
;;
YEAR)
$MP3TAG -y "$value" "$2"
;;
COMMENT)
$MP3TAG -c "$value" "$2"
;;
GENRE)
$MP3TAG -g "$value" "$2"
;;
TRACKNUMBER)
$MP3TAG -T "$value" "$2"
;;
esac
fi
done
}
main()
{
if [ $# -lt 1 ]; then
usage
exit
fi
cue_file=$1
shift
ntrack=$(cueprint -d '%N' "$cue_file")
trackno=1
NUM_FILES=0 FIELDS=
for arg in "$@"; do
case "$arg" in
*.*) NUM_FILES=$(expr $NUM_FILES + 1);;
*) FIELDS="$FIELDS $arg";;
esac
done
if [ $NUM_FILES -ne $ntrack ]; then
echo "warning: number of files does not match number of tracks"
fi
for file in "$@"; do
case $file in
*.[Ff][Ll][Aa][Cc])
vorbis $trackno "$file" $FIELDS
;;
*.[Oo][Gg][Gg])
vorbis $trackno "$file" $FIELDS
;;
*.[Mm][Pp]3)
id3 $trackno "$file" $FIELDS
;;
*.[Tt][Xx][Tt])
vorbis $trackno "$file"
;;
*.*)
echo "$file: uknown file type"
;;
esac
trackno=$(($trackno + 1))
done
}
main "$@"

View File

@@ -1,13 +1,60 @@
#!/bin/bash
source bashcols
defcol orange 202
scr_dir=$(dirname "$0")
if [[ -f $HOME/.config/music_tagmove/config ]]; then
source "$HOME/.config/music_tagmove/config"
fi
source "$scr_dir/music_tagmove_lib"
if [[ -n $MUSIC_TAGMOVE_DEVELOPER ]]; then
scr_dir=$(dirname "$0")
source "$scr_dir/music_tagmove_lib"
else
source "/usr/lib/music_tagmove_lib"
fi
SPLIT='NO'
while true; do
case "$1" in
'-n'|'--dry-run')
DRY_RUN=1
shift
;;
'-s'|'--split')
SPLIT='YES'
shift
;;
'-y'|'--yes')
YES=1
shift
;;
*)
break
;;
esac
done
source_dir="$1"
dest_root="$2"
cuesplit_all
printf "======= MUSIC TAGMOVE =======\n> Source dir: %s\n> Dest root: %s\n> Split: %s\n\n" \
"$source_dir" "$dest_root" "$SPLIT"
if [[ -z $YES ]]; then
if [[ -z $DRY_RUN ]]; then
orange printf "REAL RUN - FS WILL BE MODIFIED. CONTINUE? [y/n] "
read -r user_choice
if ! [[ $user_choice =~ ^[yY] ]]; then
exit 0
fi
fi
fi
if [[ $SPLIT == YES ]]; then
cuesplit_all
fi
tagmove
cyan echo -e "\n------- EXECUTION COMPLETE -------\n"

View File

@@ -1,6 +1,11 @@
#!/bin/bash
source bashcols
defcol violet 135
set -e
set -o pipefail
if [[ -z $DIRECTORY_FORMAT ]]; then
DIRECTORY_FORMAT='$artist/$date $album'
fi
@@ -9,8 +14,6 @@ if [[ -z $FILE_FORMAT ]]; then
FILE_FORMAT='$track $title'
fi
cuetag=$(realpath ./cuetag.sh)
global_errf=
local_errf=
@@ -30,33 +33,157 @@ function print_error_stack {
}
function info {
printf "%s %s\n" $(colthis cyan "[info]") "$*"
printf "%s %s\n" $(cyan echo "[info]") "$*"
}
function error {
global_errf=1
local_errf=1
current_error_line=$(printf "%s %s" $(colthis red "[error]") "$*")
current_error_line=$(printf "%s %s" $(red echo "[error]") "$*")
global_error_stack+=( "$current_error_line" )
>&2 printf "%s\n" "$current_error_line"
}
function clr_errf { local_errf=
function clr_errf {
local_errf=
}
function get_file_extension {
echo "${1##*.}"
}
function read_tag_flac {
local tag=${1^^}
local file="$2"
if [[ $tag == "TRACK" ]]; then
tag="TRACKNUMBER"
fi
local comment_line
comment_line=$(metaflac --list "$file" --block-type=VORBIS_COMMENT | grep -iE ":\s$tag=" | head -1)
if [[ $? -ne 0 ]]; then
error "Reading tag \"$1\" from \"$file\" failed: metaflac error"
return
fi
local result=${comment_line#*=}
if [[ -z $result ]]; then
error "Reading tag "$1" from \"$file\" failed: empty value"
return
fi
echo "$result"
}
function read_tag_id3 {
local tag="$1"
local file="$2"
declare -A id3v2_map=( [artist]=TPE1 \
[album]=TALB \
[title]=TIT2 \
[date]=TYER \
[track]=TRCK )
declare -A id3v2_map_alt=( [artist]=TP1 \
[album]=TAL \
[title]=TT2 \
[date]=TYE \
[track]=TRK )
local id3v2_content_rfc822
id3v2_content_rfc822=$(id3v2 -R "$file")
if [[ $? -ne 0 ]]; then
error "Reading tag \"$1\" from \"$file\" failed: id3v2 error"
return
fi
local no_id3tag_fmt='*No ID3 tag'
if [[ $id3v2_content_rfc822 == $no_id3tag_fmt ]]; then
read_tag_fallback "$tag" "$file"
return
else
local rfc822tag=${id3v2_map[$tag]}
if [[ -z $rfc822tag ]]; then
error "Unknown tag \"$tag\" requested for file \"$file\""
return
fi
local substr_idx=6
local line=$(grep -aE "^$rfc822tag:\s" <<<"$id3v2_content_rfc822" | head -1)
if [[ -z $line ]]; then
rfc822tag=${id3v2_map_alt[$tag]}
line=$(grep -aE "^$rfc822tag:\s" <<<"$id3v2_content_rfc822" | head -1)
substr_idx=5
fi
local result=${line:${substr_idx}}
if [[ -z $result ]]; then
error "Reading tag \"$tag\" from \"$file\" failed: empty value"
return
fi
if [[ $tag == 'track' ]]; then
result=$(grep -oE '^[[:digit:]]+' <<<"$result")
fi
echo "$result"
fi
}
function ffprobe_readtag {
local tag="$1"
local file="$2"
local result=
result=$(ffprobe -loglevel error -show_entries format_tags="$tag" -of default=noprint_wrappers=1:nokey=1 "$file")
if [[ -z $result ]]; then
result=$(ffprobe -loglevel error -show_entries stream_tags="$tag" -of default=noprint_wrappers=1:nokey=1 "$file")
fi
echo "$result"
}
function read_tag_fallback {
local tag="$1"
local file="$2"
local tag_original="$tag"
local result=
if [[ "$tag" == 'date' ]]; then
result=$(ffprobe_readtag "$tag" "$file")
if [[ -z $result ]]; then
tag='year'
fi
fi
result=$(ffprobe_readtag "$tag" "$file")
if [[ $? -ne 0 ]]; then
error "Reading tag \"$tag_original\" from \"$file\" failed (ffprobe failed)"
return
fi
if [[ -z $result ]]; then
error "Reading tag \"$tag_original\" from \"$file\" failed (result empty)"
return
fi
echo "$result"
}
function read_tag {
clr_errf
local tag="$1"
local file="$2"
local result=$(ffprobe -loglevel error -show_entries format_tags="$tag" -of default=noprint_wrappers=1:nokey=1 "$file")
if [[ $? -ne 0 ]]; then
error "Reading tag \"$tag\" from \"$file\" failed (ffprobe failed)"
return
fi
if [[ -z $result ]]; then
error "Reading tag \"$tag\" from \"$file\" failed (result empty)"
fi
echo "$result"
local ext=$(get_file_extension "$file")
shopt -s nocasematch
case "$ext" in
flac)
read_tag_flac "$tag" "$file"
;;
mp3)
read_tag_id3 "$tag" "$file"
;;
*)
read_tag_fallback "$tag" "$file"
;;
esac
}
function cuesplit_single {
@@ -115,10 +242,13 @@ function cuesplit_single {
### Strip BOM
bbe -e 's/\xEF\xBB\xBF//' "$cue" -o ".bomstripped.cue" && mv ".bomstripped.cue" "$cue"
### Uncomment DATE
sed -i 's/REM DATE/DATE/g' "$cue"
if cuebreakpoints "$cue" | shnsplit -o flac "$flac" || \
cuebreakpoints "$cue" | sed 's/$/0/' | shnsplit -o flac "$flac"; then
info "Tagging target \"$image_dir\""
if $cuetag "$cue" split-*.flac; then
if cuetag.sh "$cue" split-*.flac; then
info "Renaming source files"
mv "$cue" "$cue.ignore"
mv "$flac" "$flac.ignore"
@@ -139,25 +269,38 @@ function cuesplit_all {
}
function mv_wrap {
echo "mv" "$@"
printf "mv"
printf " \"%s\"" "$@"
echo
}
function cmd_wrap {
local cmd_prefix
if [[ -n $DRY_RUN ]]; then
cmd_prefix='noop'
else
cmd_prefix='exec'
fi
local cmd_text=$(violet printf "%s" "${@:1:1}"; printf ' "%s"' "${@:2}")
printf "%s %s\n" $(grey printf "[$cmd_prefix]") "$cmd_text"
if [[ -z $DRY_RUN ]]; then
"$@"
fi
}
function tagmove_single {
function _replace_slash { tr '/' '-' <<<"$1"; }
local_errf=
file_name="$1"
artist=$(read_tag artist "$file_name")
date=$(read_tag date "$file_name")
album=$(read_tag album "$file_name")
track=$(read_tag track "$file_name")
title=$(read_tag title "$file_name")
artist=$(_replace_slash "$(read_tag artist "$file_name")")
date=$( _replace_slash "$(read_tag date "$file_name")")
album=$( _replace_slash "$(read_tag album "$file_name")")
track=$( _replace_slash "$(read_tag track "$file_name")")
title=$( _replace_slash "$(read_tag title "$file_name")")
if [[ -n $local_errf ]]; then
error "Error reading tags for file \"$file_name\""
return
else
if [[ $global_errf ]]; then
error "Global error flag set, not performing move on file \"$file_name\""
return
fi
fi
if [[ -z $artist ]] || \
@@ -170,14 +313,35 @@ function tagmove_single {
fi
# Set fixed width for track number
track=$(printf "%02d" "${track#0}")
track=$(printf "%02d" $(cut -d'-' -f1 <<<"${track#0}"))
eval "dest_directory=\"$DIRECTORY_FORMAT\""
eval "dest_file=\"$FILE_FORMAT\""
mkdir -p "$dest_root/$dest_directory"
mv_wrap "$file_name" "$dest_root/$dest_directory/$dest_file.${file_name##*.}"
if ! [[ -d "$dest_root/$dest_directory" ]]; then
cmd_wrap mkdir -p "$dest_root/$dest_directory"
fi
cmd_wrap mv "$file_name" "$dest_root/$dest_directory/$dest_file.$(get_file_extension "$file_name")"
}
function tagmove_get_media_type {
file="$1"
ext=$(get_file_extension "$file")
shopt -s nocasematch
case $ext in
flac|alac|aac|m4a|mp3|ogg|oga|opus|wav|wma)
echo "audio"
;;
jpg|jpeg|png)
echo "image"
;;
*)
file -b --mime-type "$file" | cut -d'/' -f1
;;
esac
}
@@ -188,9 +352,18 @@ function tagmove {
fi
while read -r file; do
if [[ $(file -b --mime-type "$file") =~ ^audio/ ]]; then
tagmove_single "$file"
fi
media_type=$(tagmove_get_media_type "$file")
case "$media_type" in
audio)
tagmove_single "$file"
;;
image)
# TODO: cover move
echo "potential cover: $file"
;;
esac
done < <(find "$source_dir" -type f -not -name "*.ignore")
}