AtOM/atom
2013-03-24 04:34:56 +01:00

1956 lines
40 KiB
Bash
Executable File

#!/bin/bash
## Define exit codes
# General config errors [10-19]
ELOAD=10
EINTERVAL=11
ENOCFG=19
# Source cofig errors [20-29]
# Destination config errors [30-49]
EFORMAT=30
ECHANNEL=31
EFMTINVPARM=49
# config structures
declare -A \
destinationchannels \
destinationcopymime \
destinationformat \
destinationfrequency \
destinationid \
destinationmaxbps \
destinationnormalize \
destinationpath \
destinationquality \
destinationrename \
destinationnoresample \
destinationrenamepath \
destinationskipmime \
|| {
echo "Check your Bash version. You need >= 4.0" >&2
exit $EBASHVERS
}
declare -r \
DOCDIR=./doc \
SHAREDIR=./share
declare -r \
exampleconf=$DOCDIR/example.cfg \
schema=$SHAREDIR/schema.sql \
\
oldIFS="$IFS"
source $SHAREDIR/id3genres
shopt -s extglob
#parse arguments
OPTERR=0
while getopts ':c:l:T:F:hD' opt
do
case $opt in
c)
cffile="$OPTARG"
;;
l)
cliload="$OPTARG"
;;
T)
cliltimer="$OPTARG"
;;
F)
forceall+=("$OPTARG")
;;
h)
help
exit 0
;;
D)
(( debug++ ))
;;
:)
echo "-$OPTARG requires an argument"
help
exit 127
;;
*)
echo "Unrecognized option: -$OPTARG"
help
exit 127
;;
esac
done
#parse config
getConfigGeneral() {
case $key in
'max-load')
expr='^[0-9]*$'
if [[ $value =~ $expr ]]
then
maxload="$value"
else
echo "Invalid max-load value: $value" >&2
exit $ELOAD
fi
unset expr
;;
'load-interval')
expr='^[0-9]*$'
if [[ $value =~ $expr ]]
then
loadinterval="$value"
else
echo "Invalid load-interval value: $value" >&2
exit $EINTERVAL
fi
unset expr
;;
'temporary-directory')
tempdir="$value"
;;
'database')
database="$value"
;;
debug)
(( value > debug )) && debug=$value
;;
esac
}
getConfigSource() {
case "$key" in
'path')
sourcepath="$value"
;;
'skip')
skippeddirectories+=( "$value" )
;;
esac
}
getConfigDestination() {
case "$key" in
'path')
destinationpath["$destination"]="$value"
;;
'format')
case "$value" in
'mp3')
destinationformat["$destination"]=mp3
;;
'vorbis')
destinationformat["$destination"]=vorbis
;;
*)
echo "Unsupported destination format: $value" >2&
exit $EFORMAT
;;
esac
;;
'quality')
expr='^[0-9]*$'
if ! [[ $value =~ $expr ]]
then
echo "Invalid quality value: $value" >&2
exit $EQUALITY
fi
unset expr
case "${destinationformat["$destination"]}" in
'vorbis')
destinationquality["$destination"]="$value"
;;
*)
echo "Invalid parameter \"$key\" for format \"${destinationformat["$destination"]}\"" >&2
exit $EFMTINVPARM
;;
esac
;;
'normalize')
case $value in
'true'|'on'|'yes')
destinationnormalize["$destination"]=1
;;
'false'|'off'|'no')
destinationnormalize["$destination"]=0
;;
*)
echo "normalize takes values:" \
"'yes' ,'true' ,'on', 'no', 'false',"\
"'off'"
;;
esac
;;
'bitrate')
expr='^[0-9]*$'
if ! [[ $value =~ $expr ]]
then
echo "Invalid bitrate value: $value" >&2
exit $EQUALITY
fi
unset expr
case "${destinationformat["$destination"]}" in
'mp3')
destinationquality["$destination"]="$value"
;;
*)
echo "$Invalid parameter \"$key\" for format \"${destinationformat["$destination"]}\"" >&2
exit $EFMTINVPARM
;;
esac
;;
'channels')
expr='^[0-9]*$'
if ! [[ $value =~ $expr ]]
then
echo "Invalid channel count: $value" >&2
exit $ECHANNEL
fi
unset expr
destinationchannels["$destination"]=$value
;;
'frequency')
expr='^[0-9]*$'
if ! [[ $value =~ $expr ]]
then
echo "Invalid frequency value: $value" >&2
exit $ECHANNEL
fi
unset expr
destinationfrequency["$destination"]=$value
;;
'noresample')
case $value in
'true'|'on'|'yes')
destinationnoresample["$destination"]=1
;;
'false'|'off'|'no')
destinationnoresample["$destination"]=0
;;
*)
echo "noresample takes values:" \
"'yes' ,'true' ,'on', 'no', 'false',"\
"'off'"
;;
esac
;;
'rename')
case "$value" in
*/*)
destinationrenamepath["$destination"]="${value%/*}"
;;
esac
destinationrename["$destination"]="${value##*/}"
;;
'skip_mime-type')
destinationskipmime[$destination]="${destinationskipmime[$destination]:+${destinationskipmime[$destination]}|}$value"
;;
'copy_mime-type')
destinationcopymime[$destination]="${destinationcopymime[$destination]:+${destinationcopymime[$destination]}|}$value"
;;
'higher-than')
expr='^[0-9]*$'
if ! [[ $value =~ $expr ]]
then
echo "Invalid higher-than bitrate value: $value" >&2
exit $EMAXBPS
fi
unset expr
destinationmaxbps[$destination]="$value"
;;
esac
}
getConfig() {
while read key value
do
case $key in
'#'*)
#comment
;;
'')
#empty line
;;
'[general]')
context=General
;;
'[source]')
context=Source
;;
\[*\])
context=Destination
destination="${key#[}"
destination="${destination%]}"
;;
*)
getConfig$context
;;
esac
done < ~/.atom/atom.cfg
}
#check sanity
openDatabase() {
if [ ! -d "$tempdir" ]
then
mkdir -p "$tempdir"
fi
rm -f "$tempdir"/sqlite.{in,out}
mkfifo "$tempdir"/sqlite.{in,out}
if [ ! -f "$database" ]
then
if [ ! -d "${database%/*}" ]
then
mkdir -p "${database%/*}"
fi
sqlite3 "$database" < $schema
fi
sqlite3 -bail "$database" \
< "$tempdir/sqlite.in" \
> "$tempdir/sqlite.out" &
exec 3> "$tempdir"/sqlite.in
exec 4< "$tempdir"/sqlite.out
if (( debug > 2 ))
then
exec 5>&3
exec 3> >(tee -a $tempdir/debug.log >&5)
fi
echo 'PRAGMA foreign_keys = ON;' >&3
}
closeDatabase() {
echo .quit >&3
(( debug )) && echo -n "Waiting for SQLite to terminate... "
wait
(( debug )) && echo OK
exec 3>&-
exec 4<&-
rm "$tempdir"/sqlite.{in,out}
}
Select() {
#Select table [col1 [col2 [..]]] < WHERE_key WHERE_operator WHERE_value
# [WHERE_key WHERE_operator WHERE_value
# […]]
local \
table="$1" \
col \
columns \
operator \
results \
where_statement
shift
for col
do
(( ${#columns} )) && columns+=','
columns+="$col"
done
while read key operator value
do
(( ${#where_statement} )) && where_statement+=( "AND" )
where_statement+=( "$key $operator "'"'"${value//\"/\"\"}"'"' )
done
echo "SELECT IFNULL(" \
"(SELECT $columns FROM $table" \
"WHERE ${where_statement[@]})" \
",'SQL::Select:not found'" \
");" >&3
read -u 4 results
if ! [[ $results == "SQL::Select:not found" ]]
then
echo "$results"
else
return 1
fi
}
Insert() {
#Insert table [no_id] < key value
# [key value
# […]]
local \
table="$1" \
no_id="${2:-0}" \
insert_keys \
insert_values \
results
while read key value
do
(( ${#insert_keys} )) && insert_keys+=","
insert_keys+='`'"$key"'`'
(( ${#insert_values} )) && insert_values+=","
case $value in
'NULL')
insert_values+="NULL"
;;
+([0-9])?(.+([0-9])))
insert_values+=$value
;;
*)
insert_values+='"'"${value//\"/\"\"}"'"'
;;
esac
done
echo "INSERT INTO $table" \
"( $insert_keys )" \
"VALUES" \
"( $insert_values );" >&3
(( no_id )) || {
echo 'SELECT LAST_INSERT_ROWID();' >&3
read -u 4 results
echo "$results"
}
}
Update(){
#Update table set_key set_value [set_key set_value […]] < where_key where_operator where_value
# [where_key where_operator where_value
# […]]
local \
table="$1" \
key \
argument \
operator \
value \
set_statement \
where_keys \
where_values \
what \
where_statement \
results
shift
what=key
for argument
do
case $what in
key)
set_statement="${set_statement:+${set_statement},}\`$argument\`"
what=value
;;
value)
case $argument in
'NULL')
set_statement+=" = NULL"
;;
+([0-9])?(.+([0-9])))
set_statement+=" = $argument"
;;
*)
set_statement+=" = "'"'"${argument//\"/\"\"}"'"'
;;
esac
what=key
;;
esac
done
while read key operator value
do
(( ${#where_statement} )) && where_statement+=( "AND" )
case $value in
'NULL')
where_statement+=( "$key is NULL" )
;;
+([0-9.]))
where_statement+=( "$key $operator $value" )
;;
*)
where_statement+=( "$key $operator "'"'"${value//\"/\"\"}"'"' )
;;
esac
done
echo "UPDATE '$table' SET" \
"$set_statement" \
"WHERE" \
"${where_statement[@]}" \
";" >&3
}
InsertIfUnset() {
#InsertIfUnset table [no_id] < key value \n key value
local \
table="$1" \
no_id="${2:-0}" \
column \
key \
keys \
results \
value \
values
while read key value
do
keys+=( "$key" )
values+=( "$value" )
done
if (( no_id ))
then
column="${keys[0]}"
else
column='id'
fi
if ! results=$(
Select "$table" "$column" < <(
for key in ${!keys[@]}
do
echo "${keys[$key]}" = "${values[$key]}"
done
)
)
then
results=$(
Insert "$table" < <(
for key in ${!keys[@]}
do
echo "${keys[$key]}" "${values[$key]}"
done
)
)
fi
echo "$results"
}
InsertOrUpdate() {
#InsertOrUpdate table set_key set_value [set_key set_value […]] < where_key where_value
# [where_key where_value
# […]]
local \
table="$1" \
argument \
key \
keys \
set_keys \
set_values \
value \
values \
what \
results
shift
what=key
for argument
do
case $what in
key)
set_keys+=( "$argument" )
what=value
;;
value)
set_values+=( "$argument" )
what=key
;;
esac
done
while read key value
do
keys+=( "$key" )
values+=( "$value" )
done
if results=$(
Select "$table" ${keys[0]} < <(
for key in ${!keys[@]}
do
echo "${keys[$key]}" = "${values[$key]}"
done
)
)
then
Update "$table" "$@" < <(
for key in ${!keys[@]}
do
echo "${keys[$key]}" = "${values[$key]}"
done
)
else
results=$(
Insert "$table" < <(
for key in ${!set_keys[@]}
do
echo "${set_keys[$key]}" "${set_values[$key]}"
done
for key in ${!keys[@]}
do
echo "${keys[$key]}" "${values[$key]}"
done
)
)
fi
echo "$results"
}
Delete() {
#Delete table < where_key where_operator where_value
# [where_key where_operator where_value
# […]]
local \
table="$1" \
key \
operator \
value \
where_statement \
results
while read key operator value
do
(( ${#where_statement} )) && where_statement+=( "AND" )
if [[ $value == NULL ]]
then
where_statement+=( "$key is NULL" )
else
where_statement+=( "$key $operator "'"'"${value//\"/\"\"}"'"' )
fi
done
echo "DELETE from $table WHERE ${where_statement[@]};" >&3
}
createDestinations() {
for destination in ${!destinationpath[@]}
do
if ! [ -d "${destinationpath["$destination"]}" ]
then
if ! mkdir -p "${destinationpath["$destination"]}"
then
echo "$destination: Could not create ${destinationpath["$destination"]}!"
exit $EINVDEST
fi
fi
destinationid["$destination"]=$(
InsertIfUnset destinations <<<"name $destination"
)
done
}
progressSpin() {
case $(( ++count % 40 )) in
0) echo -ne '\b|' ;;
10) echo -ne '\b/' ;;
20) echo -en '\b-' ;;
30) echo -ne '\b\\' ;;
*) ;;
esac
}
getFiles() {
scantime=$(date +%s)
for prune_expression in "${skippeddirectories[@]}"
do
prunes+="-path $sourcepath$prune_expression -prune -o "
done
echo -n "Scanning $sourcepath... "
# We probably have thousands of files, don't waste time on disk writes
echo 'BEGIN TRANSACTION;' >&3
while read time size filename
do
if ! Select source_files id >/dev/null <<-EOWhere
filename = $filename
mime_type > 0
last_change = $time
EOWhere
then
mimetype=$(file -b --mime-type "$sourcepath/$filename")
mimetypeid=$(
InsertIfUnset mime_types <<-EOInsert
mime_text $mimetype
EOInsert
)
InsertOrUpdate source_files \
last_change $time \
size $size \
last_seen $scantime \
mime_type $mimetypeid \
>/dev/null \
<<-EOWhere
filename $filename
EOWhere
(( ++new ))
if (( new % 1000 == 0 ))
then
echo 'COMMIT;BEGIN TRANSACTION;' >&3
(( debug )) \
&& echo -ne "\bCommitted $count files... "
fi
else
Update source_files last_seen $scantime <<-EOWhere
filename = $filename
EOWhere
fi
progressSpin
done < <(
find "$sourcepath" $prunes -type f -printf "%T@ %s %P\n"
)
echo 'COMMIT;' >&3
echo -e "\r${count:-0} files found, ${new:=0} new or changed."
unset count
}
updateMimes() {
Update mime_actions action 1 <<<"action != 1"
for destination in ${!destinationskipmime[@]}
do
IFS='|'
for mime_type in ${destinationskipmime["$destination"]}
do
IFS="$oldIFS"
Update mime_type_actions action 0 >/dev/null < <(
cat <<-EOWhere
destination_id = ${destinationid["$destination"]}
mime_text LIKE ${mime_type//\*/%}
EOWhere
)
done
done
for destination in ${!destinationcopymime[@]}
do
IFS='|'
for mime_type in ${destinationcopymime["$destination"]}
do
IFS="$oldIFS"
Update mime_type_actions action 2 >/dev/null < <(
cat <<-EOWhere
destination_id = ${destinationid["$destination"]}
mime_text LIKE ${mime_type//\*/%}
EOWhere
)
done
done
}
removeObsoleteFiles() {
Delete source_files <<-EOWhere
last_seen < $scantime
EOWhere
}
getRateChannelSoxi() {
rate=$(soxi -r "$sourcepath/$filename" 2>/dev/null)
channels=$(soxi -c "$sourcepath/$filename" 2>/dev/null)
}
getRateChannelMPC() {
while read key value garbage
do
case $key in
'samplerate:')
rate=$value
;;
'channels:')
channels=$value
;;
esac
done < <(
mpcdec "$sourcepath/$filename" -i 2>&1
)
}
gettag() {
echo -e "$infos" \
| sed -n "/^${1}=/I{s/^${1}=//I;p;q}"
}
getInfosMP3_version='ID3-1'
tagreaders+=( "$getInfosMP3_version" )
getInfos::MP3() {
tagreader="$getInfosMP3_version"
infos=$(
soxi "$sourcepath/$filename" 2>/dev/null \
| sed 's/ *: /=/'
)
album=$(gettag album)
artist=$(gettag artist)
genre=$(gettag genre)
title=$(gettag title)
tracknum=$(gettag tracknumber)
year=$(gettag year)
expr='^[0-9]*$'
if [[ $genre =~ $expr ]]
then
genre="${id3genres[$genre]}"
fi
infos="${infos//: /=}"
channels=$(gettag channels)
rate=$(gettag 'sample rate')
bitrate=$(gettag 'bit rate')
bitrate=${bitrate%%.*}
bitrate=${bitrate%k}
}
getInfosOgg_version='Ogg-1'
tagreaders+=( "$getInfosOgg_version" )
getInfos::Ogg() {
tagreader="$getInfosOgg_version"
infos=$(
ogginfo "$sourcepath/$filename" \
| sed 's/\t//'
)
albumartist=$(gettag albumartist)
album=$(gettag album)
artist=$(gettag artist)
composer=$(gettag composer)
disc=$(gettag discnumber)
genre=$(gettag genre)
performer=$(gettag performer)
title=$(gettag title)
tracknum=$(gettag tracknumber)
tracktotal=$(gettag tracktotal)
if [ -n "$tracknum" -a -n "$tracktotal" ]
then
tracknum="$tracknum/$tracktotal"
fi
year=$(gettag date)
infos="${infos//: /=}"
rate=$(gettag rate|head -n1)
channels=$(gettag channels|head -n1)
bitrate=$(gettag 'nominal bitrate')
bitrate=${bitrate%%,*}
}
getInfosFLAC_version='FLAC-1'
tagreaders+=( "$getInfosFLAC_version" )
getInfos::FLAC() {
tagreader="$getInfosFLAC_version"
infos=$(
metaflac \
--show-tag=ALBUM \
--show-tag=ALBUMARTIST \
--show-tag=ARTIST \
--show-tag=COMPOSER \
--show-tag=DATE \
--show-tag=DISCNUMBER \
--show-tag=GENRE \
--show-tag=PERFORMER \
--show-tag=TITLE \
--show-tag=TRACKNUMBER \
--show-tag=TRACKTOTAL \
"$sourcepath/$filename"
)
albumartist=$(gettag albumartist)
album=$(gettag album)
artist=$(gettag artist)
composer=$(gettag composer)
disc=$(gettag discnumber)
genre=$(gettag genre)
performer=$(gettag performer)
title=$(gettag title)
tracknum="$(gettag tracknumber)/$(gettag tracktotal)"
year=$(gettag date)
if [ -n "$tracknum" -a -n "$tracktotal" ]
then
tracknum="$tracknum/$tracktotal"
fi
year=$(gettag DATE)
{
read rate
read channels
} < <(
metaflac \
--show-sample-rate \
--show-channels \
"$sourcepath/$filename"
)
}
getInfosAPE_version='APE-1'
tagreaders+=( "$getInfosAPE_version" )
getInfos::APE() {
# I was not able to find a decent cli tool to read APE tags.
# This is raw but works for the very few MusePack files I got.
#
# Please tell me if you know of any good tool.
tagreader="$getInfosAPE_version"
IFS='='
while read tag value
do
IFS="$oldIFS"
case $tag in
[Aa][Ll][Bb][Uu][Mm]' '[Aa][Rr][Tt][Ii][Ss][Tt])
albumartist="$value"
;;
[Aa][Rr][Tt][Ii][Ss][Tt])
artist="$value"
;;
[Yy][Ee][Aa][Rr])
year="$value"
;;
[Aa][Ll][Bb][Uu][Mm])
album="$value"
;;
[Tt][Ii][Tt][Ll][Ee])
title="$value"
;;
[Tt][Rr][Aa][Cc][Kk])
tracknum="$value"
;;
[Gg][Ee][Nn][Rr][Ee])
genre="$value"
;;
[Cc][Oo][Mm][Pp][Oo][Ss][Ee][Rr])
composer="$value"
;;
[Pp][Ee][Rr][Ff][Oo][Rr][Mm][Ee][Rr])
performer="$value"
;;
*)
;;
esac
IFS='='
done < <(
IFS="$oldIFS"
sed \
's/APETAGEX/\n/;s/[\x00\-\x1F]\x00\+/\n/g;s/\x00/=/g' \
"$sourcepath/$filename" \
| egrep -i \
'^(Album Artist|Artist|Year|Album|Title|Track|Genre|Composer|Performer)='
)
IFS="$oldIFS"
}
tryAPE() {
grep -q 'APETAGEX' \
"$sourcepath/$filename" \
&& type=APE
}
getTags_version='unknown-1'
tagreaders+=( "$getTags_version" )
getTags() {
unset type
case "$mimetype" in
audio/mpeg)
type=MP3
;;
application/ogg)
type=Ogg
;;
audio/x-flac)
type=FLAC
;;
*)
extendedtype=$(file -b "$sourcepath/$filename")
case "$extendedtype" in
*' ID3 '*)
type=MP3
;;
*'Musepack '*)
getRateChannelMPC
tryAPE
;;
*)
getRateChannelSoxi
tryAPE
;;
esac
;;
esac
if [ -n "$type" ]
then
getInfos::$type
else
tagreader=$getTags_version
fi
}
checkCopy() {
(
[ -z "${destinationfrequency[$destination]}" ] \
|| (( ${rate:-0} == ${destinationfrequency[$destination]} ))
) && (
[ -z "${destinationchannels[$destination]}" ] \
|| (( ${channels:-0} == ${destinationchannels[$destination]} ))
) && (
(( ${bitrate:-1000} == ${destinationquality[$destination]} )) \
|| (
[ -n "${destinationmaxbps[$destination]}" ] \
|| ((
${bitrate:-1000}
<= ${destinationmaxbps[$destination]:-0}
))
)
)
}
decodeSox() {
if (( ${destinationnormalize["$destination"]} ))
then
soxoptions_in+=" --norm"
fi
if [ -n "${destinationfrequency["$destination"]}" ] \
&& (( ${rate:-0} != ${destinationfrequency["$destination"]} ))
then
soxoptions_out+=" -r ${destinationfrequency["$destination"]}"
fi
if [ -n "${destinationchannels["$destination"]}" ] \
&& (( ${channels:-0} != ${destinationchannels["$destination"]} ))
then
soxoptions_out+=" -c ${destinationchannels["$destination"]}"
fi
tmpfile="$fileid${soxoptions_in// /}${soxoptions_out// /}"
soxoptions_in+=' --single-threaded'
soxoptions_out+=" --temp \"$tempdir\""
if [ -n "$1" ]
then
origin="$1"
else
origin="$sourcepath/$filename"
fi
commandline="sox $soxoptions_in \"$origin\" $soxoptions_out \"$tempdir/$tmpfile.wav\""
}
decodeMpcdec() {
tmpfile="${fileid}mpcdec"
commandline="mpcdec \"$sourcepath/$filename\" \"$tempdir/$tmpfile.wav\""
}
decodeFile() {
if ! decodetaskid=$(
Select tasks id <<<"key = $tmpfile"
)
then
decodetaskid=$(
Insert tasks <<-EOInsert
key $tmpfile
source_file $fileid
command_line $commandline
status 0
EOInsert
)
progressSpin
fi
if (( sox_needed ))
then
cleanup="$tempdir/$tmpfile"
decodeSox "$tempdir/$tmpfile.wav"
if ! soxtaskid=$(
Select tasks id <<<"key = $tmpfile"
)
then
soxtaskid=$(
Insert tasks <<-EOInsert
key $tmpfile
source_file $fileid
command_line $commandline
requires $decodetaskid
required $decodetaskid
status 0
cleanup $cleanup
EOInsert
)
progressSpin
fi
fi
}
copyFile() {
tmpfile="${fileid}copy$destination"
extension="${filename##*.}"
commandline="cp -al \"$sourcepath/$filename\" \"$destdir/$destfile.$extension\""
commandline+=" 2>/dev/null"
commandline+=' AtOM:OR '
commandline+="cp -a \"$sourcepath/$filename\" \"$destdir/$destfile\""
copytaskid=$(
Insert tasks <<-EOInsert
key ${fileid}copy$destination
source_file $fileid
command_line $commandline
status 0
EOInsert
)
progressSpin
}
sanitizeFile() {
string="$1"
# Filenames can't contain /
string="${strings//\// }"
if (( ${destinationfat32compat[$destination]} ))
then
# Filenames can't contain:
string=${string//\?/ }
string=${string//\\/ }
string=${string//</ }
string=${string//>/ }
string=${string//:/ }
string=${string//\*/ }
string=${string//|/ }
string=${string//\"/ }
# Filenames can't begin or end with ' '
string=${string/#+( )/}
string=${string//+( )./.}
string=${string//.+( )/.}
fi
echo "$string"
}
getDestDir() {
destdir="${destinationpath[$destination]}/"
if [ -n "${destinationrenamepath[$destination]}" ]
then
destdir+="${destinationrenamepath[$destination]//%\{album\}/$album}"
replace=$(sanitizeFile "$albumartist")
destdir="${destdir//%\{albumartist\}/$replace}"
replace=$(sanitizeFile "$artist")
destdir="${destdir//%\{artist\}/$replace}"
replace=$(sanitizeFile "$genre")
destdir="${destdir//%\{genre\}/$replace}"
replace=$(sanitizeFile "$title")
destdir="${destdir//%\{title\}/$replace}"
tracknumber="${track%/*}"
replace=$(sanitizeFile "$tracknumber")
destdir="${destdir//%\{track\}/$replace}"
replace=$(sanitizeFile "$year")
destdir="${destdir//%\{year\}/$replace}"
else
destdir+="${filename%/*}"
fi
if ! [ -d "$destdir" ]
then
mkdir -p "$destdir"
fi
}
getDestFile() {
if [ -n "${destinationrename[$destination]}" ]
then
destfile="${destinationrename[$destination]//%\{album\}/$album}"
destfile="${destfile//%\{albumartist\}/$albumartist}"
destfile="${destfile//%\{artist\}/$artist}"
destfile="${destfile//%\{genre\}/$genre}"
destfile="${destfile//%\{title\}/$title}"
tracknumber="${track%/*}"
destfile="${destfile//%\{track\}/$tracknumber}"
destfile="${destfile//%\{year\}/$year}"
else
destfile="${filename##*/}"
destfile="${destfile%.*}"
fi
destfile=$(sanitizeFile "$destfile")
}
encodeFile::mp3() {
lameopts="--quiet -v --abr ${destinationquality[$destination]}"
[ -n "$album" ] && lameopts+=" --tl \"$album\""
[ -n "$artist" ] && lameopts+=" --ta \"$artist\""
[ -n "$genre" ] && lameopts+=" --tg \"$genre\""
[ -n "$title" ] && lameopts+=" --tt \"$title\""
[ -n "$track" ] && lameopts+=" --tn \"$track\""
[ -n "$year" ] && lameopts+=" --ty \"$year\""
if (( ${destinationnoresample[$destination]:-0} == 1 ))
then
# If 'rate' is not one of these value, it cannot be encoded to
# MP3, in which case we'd be better of letting lame decide which
# rate to use.
if [ -n "${destinationfrequency["$destination"]}" ]
then
case ${destinationfrequency["$destination"]} in
48000) lameopts+=" --resample 48" ;;
44100) lameopts+=" --resample 44.1" ;;
32000) lameopts+=" --resample 32" ;;
24000) lameopts+=" --resample 24" ;;
22050) lameopts+=" --resample 22.05" ;;
16000) lameopts+=" --resample 16" ;;
12000) lameopts+=" --resample 12" ;;
11025) lameopts+=" --resample 11.025" ;;
8000) lameopts+=" --resample 8" ;;
esac
else
case $rate in
48000) lameopts+=" --resample 48" ;;
44100) lameopts+=" --resample 44.1" ;;
32000) lameopts+=" --resample 32" ;;
24000) lameopts+=" --resample 24" ;;
22050) lameopts+=" --resample 22.05" ;;
16000) lameopts+=" --resample 16" ;;
12000) lameopts+=" --resample 12" ;;
11025) lameopts+=" --resample 11.025" ;;
8000) lameopts+=" --resample 8" ;;
esac
fi
fi
encodetaskid=$(
Insert tasks <<-EOInsert
key ${fileid}lame$destination
requires ${soxtaskid:-$decodetaskid}
required ${soxtaskid:-$decodetaskid}
fileid $destfileid
filename $destdir/$destfile.mp3
command_line lame $lameopts "$tempdir/$tmpfile.wav" "$destdir/$destfile.mp3"
cleanup "$tempdir/$tmpfile.wav"
source_file $fileid
status 0
EOInsert
)
progressSpin
}
encodeFile::vorbis() {
oggencopts="-Q -q ${destinationquality[$destination]}"
[ -n "$albumartist" ] && oggencopts+=" -c \"ALBUMARTIST=$albumartist\""
[ -n "$album" ] && oggencopts+=" -l \"$album\""
[ -n "$artist" ] && oggencopts+=" -a \"$artist\""
[ -n "$composer" ] && oggencopts+=" -c \"COMPOSER=$composer\""
[ -n "$disc" ] && oggencopts+=" -c \"DISCNUMBER=$disc\""
[ -n "$genre" ] && oggencopts+=" -G \"$genre\""
[ -n "$performer" ] && oggencopts+=" -c \"PERFORMER=$performer\""
[ -n "$title" ] && oggencopts+=" -t \"$title\""
[ -n "$track" ] && oggencopts+=" -N \"$track\""
[ -n "$year" ] && oggencopts+=" -d \"$year\""
encodetaskid=$(
Insert tasks <<-EOInsert
key ${fileid}oggenc$destination
requires ${soxtaskid:-$decodetaskid}
required ${soxtaskid:-$decodetaskid}
fileid $destfileid
filename $destdir/$destfile.ogg
command_line oggenc $oggencopts -o "$destdir/$destfile.ogg" "$tempdir/$tmpfile.wav"
cleanup "$tempdir/$tmpfile.wav"
source_file $fileid
status 0
EOInsert
)
progressSpin
}
checkFinished() {
#retrieve info from finished transcodeFile()
#update counters / metadata
:
}
checkLoad() {
#if load > threshold
# decrease concurrency
#elif load < threshold
# increase concurrency
#fi
:
}
readUserInput() {
#read + / - / q(uit) / p(ause)
#initiate shutdown / change load threshold / SIGSTOP all children / SIGCONT all children
:
}
transcodeLauncher() {
checkLoad
checkFinished
#until running processes < max processes
#do
checkLoad
checkFinished
readUserInput
#done
transcodeFile &
#update counter / metadata
}
worker() {
set +e
while :
do
echo work
read line
if [[ $line == AtOM:Die ]]
then
break
elif [[ $line == AtOM:Sleep ]]
then
sleep 1
continue
fi
taskid=${line%%|*}
rest="${line#*|}|"
sourcefileid=${rest%%|*}
rest=${rest#*|}
required=${rest%%|*}
rest=${rest#*|}
commandline=${rest%%|*}
rest=${rest#*|}
cleanup=${rest%%|*}
rest=${rest#*|}
destfileid=${rest%%|*}
rest=${rest#*|}
destfilename=${rest%%|*}
rest=${rest#*|}
if eval ${commandline/AtOM:OR/||} >/dev/null 2>>"$tempdir/errors.log"
then
echo "finished $taskid|$sourcefileid|$destfileid|$destfilename"
else
echo "failed $taskid"
[ -n "$filename" ] \
&& eval rm -f $filename
fi
if [ -n "$cleanup" -a -n "$required" ]
then
echo "cleanup $required"
read answer
if (( answer == 1 ))
then
eval rm -f $cleanup
fi
fi
done
return
}
master() {
for workerid in ${!workers[@]}
do
if read -t0.001 -u$((200+workerid)) workercommand workerquery
then
break
fi
done
(( ${#workers[@]} == 0 )) && break
if [ -n "$workercommand" ]
then
case $workercommand in
'work')
if [ -n "$quit" ]
then
eval echo AtOM:Die '>&'$((100+workerid))
wait ${workers[workerid]}
eval $((100+workerid))'>&-'
eval $((200+workerid))'<&-'
rm "$tempdir"/worker${workerid}{in,out}
unset workers[workerid]
elif (( active < concurrency ))
then
echo '
SELECT COUNT(*)
FROM tasks
WHERE status = 0;
SELECT COUNT(*)
FROM tasks
WHERE status = 0
AND requires is NULL;
SELECT
id,
source_file,
required,
command_line,
cleanup,
fileid,
filename
FROM tasks
WHERE status = 0
AND requires is NULL
ORDER BY source_file
LIMIT 1;
' >&3
read -u4 remaining
read -u4 ready
if (( remaining == 0 ))
then
eval echo AtOM:Die '>&'$((100+workerid))
wait ${workers[workerid]}
eval $((100+workerid))'>&-'
eval $((200+workerid))'<&-'
rm "$tempdir"/worker${workerid}{in,out}
unset workers[workerid]
continue
elif (( ready == 0 ))
then
line=AtOM:Sleep
else
(( ++active ))
read -u4 line
taskid=${line%%|*}
Update tasks status 1 <<<"id = $taskid"
fi
eval echo '"$line" >&'$((100+workerid))
else
eval echo AtOM:Die '>&'$((100+workerid))
wait ${workers[workerid]}
eval $((100+workerid))'>&-'
eval $((200+workerid))'<&-'
rm "$tempdir"/worker${workerid}{in,out}
unset workers[workerid]
fi
;;
?(f)'inished')
(( active-- )) || true
taskid=${workerquery%%|*}
rest="${workerquery#*|}|"
sourcefileid=${rest%%|*}
rest=${rest#*|}
destfileid=${rest%%|*}
rest=${rest#*|}
destfilename=${rest%%|*}
Delete tasks <<<"id = $taskid"
if [ -n "$destfilename" ]
then
echo \
"UPDATE destination_files" \
"SET filename=\"${destfilename//\"/\"\"}\"," \
" last_change=(" \
" SELECT last_change" \
" FROM source_files" \
" WHERE id=$sourcefileid" \
" )" \
"WHERE id=$destfileid;" \
>&3
fi
;;
'failed')
(( --active )) || true
(( ++failed ))
taskid=$workerquery
faildepends=$(
Select tasks 'COUNT(*)' <<-EOWhere
requires = taskid
EOWhere
)
(( failed+=faildepends ))
Update tasks status 2 <<<"id = $taskid"
Update tasks status 2 <<<"requires = $taskid"
;;
'cleanup')
required=$workerquery
echo "SELECT COUNT(*)
FROM tasks
WHERE ( status = 0 OR status = 1 )
AND required = $required;">&3
read -u4 count
if (( count == 0 ))
then
eval echo 1 '>&'$((100+workerid))
else
eval echo 0 '>&'$((100+workerid))
fi
;;
esac
fi
}
#UI
if [ ! -f ~/.atom/atom.cfg ]
then
if [ ! -d ~/.atom ]
then
mkdir -p ~/.atom
fi
sed "s:%HOME%:$HOME:" "$exampleconf" > ~/.atom/atom.cfg
cat >&2 <<-EOCfgNotice
No configuration file found!
An example file has been created as ~/.atom/atom.cfg.
You should change it to your likings using you favorite editor.
Bailing out.
EOCfgNotice
exit $ENOCFG
fi
getConfig
set -eH
if (( debug ))
then
cat <<-EOF
General|Load|$maxload
|Load Interval|$loadinterval
|Temp Dir|$tempdir
|Database|$database
|Debug|$debug
Source|Path|$sourcepath
EOF
for prune_expression in "${skippeddirectories[@]}"
do
echo "|Skipped directory|$prune_expression"
done
for destination in ${!destinationpath[@]}
do
cat <<-EOF
$destination|Path|${destinationpath[$destination]}
|Format|${destinationformat[$destination]}
|Quality|${destinationquality[$destination]}
|Normalize|${destinationnormalize[$destination]}
|Channels|${destinationchannels[$destination]}
|Frequency|${destinationfrequency[$destination]}
|Path Change|${destinationrenamepath[$destination]}
|File Rename|${destinationrename[$destination]}
EOF
echo "|Skipped mime-type|${destinationskipmime[$destination]//\|/
|Skipped mime-type|}"
echo "|Copied mime-type|${destinationcopymime[$destination]//\|/
|Copied mime-type|}"
done
fi |column -t -s'|' -n
openDatabase
createDestinations
getFiles
updateMimes
removeObsoleteFiles
# get files
for reader in "${tagreaders[@]}"
do
tagreaderclause+="${tagreaderclause:+ AND }NOT tags.tagreader = \"$reader\""
done
echo '
SELECT COUNT(DISTINCT source_files.filename)
FROM source_files
INNER JOIN destination_files
ON destination_files.source_file_id=source_files.id
INNER JOIN destinations
ON destination_files.destination_id=destinations.id
INNER JOIN mime_type_actions
ON destinations.id=mime_type_actions.destination_id
INNER JOIN tags
ON source_files.id=tags.source_file
WHERE mime_type_actions.id = source_files.mime_type
AND (
CAST(tags.last_change AS TEXT)
<>
CAST(source_files.last_change AS TEXT)
OR ('"$tagreaderclause"')
)
AND mime_type_actions.action = 1;' >&3
read -u4 filecount
echo '
SELECT DISTINCT
source_files.id,
source_files.last_change,
mime_type_actions.mime_text,
source_files.filename
FROM source_files
INNER JOIN destination_files
ON destination_files.source_file_id=source_files.id
INNER JOIN destinations
ON destination_files.destination_id=destinations.id
INNER JOIN mime_type_actions
ON destinations.id=mime_type_actions.destination_id
INNER JOIN tags
ON source_files.id=tags.source_file
WHERE mime_type_actions.id = source_files.mime_type
AND (
CAST(tags.last_change AS TEXT)
<>
CAST(source_files.last_change AS TEXT)
OR ('"$tagreaderclause"')
)
AND mime_type_actions.action = 1;
SELECT "AtOM:NoMoreFiles";' >&3
read -u4 line
while ! [[ $line = AtOM:NoMoreFiles ]]
do
tagfiles+=("$line")
read -u4 line
done
echo 'BEGIN TRANSACTION;' >&3
for line in "${tagfiles[@]}"
do
sourcefileid=${line%%|*}
rest=${line#*|}
lastchange=${rest%%|*}
rest=${rest#*|}
mimetype=${rest%%|*}
filename=${rest#*|}
echo -en "\rTags: $((++count*100/filecount))%"
if (( count % 1000 == 0 ))
then
echo 'COMMIT;BEGIN TRANSACTION;' >&3
(( debug )) \
&& echo -n " $count files read, committing..."
fi
if getTags
then
Update tags \
album "${album:-NULL}" \
albumartist "${albumartist:-NULL}" \
artist "${artist:-NULL}" \
composer "${composer:-NULL}" \
disc "${disc:-NULL}" \
genre "${genre:-NULL}" \
performer "${performer:-NULL}" \
title "${title:-NULL}" \
track "${tracknum:-NULL}" \
year "${year:-NULL}" \
last_change "$lastchange" \
rate "${rate:-NULL}" \
channels "${channels:-NULL}" \
bitrate "${bitrate:-NULL}" \
tagreader "$tagreader" \
>/dev/null <<<"source_file = $sourcefileid"
unset genre \
albumartist \
year \
album \
disc \
artist \
tracknum \
title \
composer \
performer \
rate \
bitrate \
channels
fi
done
echo 'COMMIT;' >&3
echo -e "\rRead tags from ${count:-0} files."
unset count tagfiles
echo '
CREATE TEMPORARY TABLE tasks(
id INTEGER PRIMARY KEY,
key TEXT UNIQUE,
source_file INTEGER,
fileid INTEGER,
filename TEXT,
command_line TEXT,
requires INTEGER,
required INTEGER,
status INTEGER NOT NULL,
cleanup TEXT,
FOREIGN KEY(requires) REFERENCES tasks(id)
ON DELETE SET NULL
);
CREATE INDEX tasks_by_key ON tasks ( key );
CREATE INDEX tasks_by_sourcefile ON tasks ( source_file );
' >&3
echo '
SELECT COUNT(source_files.id)
FROM source_files
INNER JOIN destination_files
ON source_files.id
= destination_files.source_file_id
INNER JOIN destinations
ON destination_files.destination_id=destinations.id
INNER JOIN mime_type_actions
ON mime_type_actions.id = source_files.mime_type
INNER JOIN tags
ON source_files.id = tags.source_file
WHERE CAST(destination_files.last_change AS TEXT)
<> CAST(source_files.last_change AS TEXT)
AND mime_type_actions.destination_id = destinations.id
AND mime_type_actions.action = 1;' >&3
read -u4 filecount
echo '
SELECT
source_files.id,
source_files.filename,
mime_type_actions.mime_text,
destinations.name,
destination_files.id,
tags.rate,
tags.channels,
tags.bitrate,
tags.genre,
tags.albumartist,
tags.year,
tags.album,
tags.disc,
tags.artist,
tags.track,
tags.title,
tags.composer,
tags.performer
FROM source_files
INNER JOIN destination_files
ON source_files.id
= destination_files.source_file_id
INNER JOIN destinations
ON destination_files.destination_id=destinations.id
INNER JOIN mime_type_actions
ON mime_type_actions.id = source_files.mime_type
INNER JOIN tags
ON source_files.id = tags.source_file
WHERE CAST(destination_files.last_change AS TEXT)
<> CAST(source_files.last_change AS TEXT)
AND mime_type_actions.destination_id = destinations.id
AND mime_type_actions.action = 1;
SELECT "AtOM:NoMoreFiles";' >&3
read -u4 line
while ! [[ $line = AtOM:NoMoreFiles ]]
do
decodefiles+=("$line|")
read -u4 line
done
echo -n 'Creating tasks... '
echo 'BEGIN TRANSACTION;' >&3
for line in "${decodefiles[@]}"
do
fileid=${line%%|*}
rest=${line#*|}
filename=${rest%%|*}
rest=${rest#*|}
mimetype=${rest%%|*}
rest=${rest#*|}
destination=${rest%%|*}
rest=${rest#*|}
destfileid=${rest%%|*}
rest=${rest#*|}
rate=${rest%%|*}
rest=${rest#*|}
channels=${rest%%|*}
rest=${rest#*|}
bitrate=${rest%%|*}
rest=${rest#*|}
genre=${rest%%|*}
rest=${rest#*|}
albumartist=${rest%%|*}
rest=${rest#*|}
year=${rest%%|*}
rest=${rest#*|}
album=${rest%%|*}
rest=${rest#*|}
disc=${rest%%|*}
rest=${rest#*|}
artist=${rest%%|*}
rest=${rest#*|}
track=${rest%%|*}
rest=${rest#*|}
title=${rest%%|*}
rest=${rest#*|}
composer=${rest%%|*}
rest=${rest#*|}
performer=${rest%%|*}
unset rest
case "$mimetype" in
'audio/mpeg')
if [[ ${destinationformat[$destination]} = mp3 ]] \
&& checkCopy
then
copied=1
else
decodeSox
fi
;;
'application/ogg')
if [[ ${destinationformat[$destination]} = vorbis ]] \
&& checkCopy
then
copied=1
else
decodeSox
fi
;;
'audio/x-flac')
decodeSox
;;
*)
extendedtype=$(file -b "$sourcepath/$filename")
case "$extendedtype" in
*'Musepack '*)
decodeMpcdec
if (( ${destinationnormalize["$destination"]}))\
|| (
[ -n "${destinationfrequency["$destination"]}" ]\
&& (( ${rate:-0} != ${destinationfrequency["$destination"]}))\
) || (
[ -n "${destinationchannels["$destination"]}" ]\
&& (( ${channels:-0} != ${destinationchannels["$destination"]} ))
)
then
sox_needed=1
fi
;;
*)
decodeSox
;;
esac
;;
esac
if ! (( copied ))
then
decodeFile
fi
getDestDir
getDestFile
if (( copied ))
then
copyFile
else
encodeFile::${destinationformat[$destination]}
fi
unset \
album \
albumartist \
artist \
bitrate \
channels \
commandline \
composer \
copied \
decodetaskid \
destfileid \
destination \
disc \
fileid \
filename \
mimetype \
performer \
rate \
rest \
sox_needed \
soxoptions_in \
soxoptions_out \
soxtaskid \
title \
track \
year \
tmpfile
done
echo 'COMMIT;' >&3
echo -e "\rCreated ${count:-0} tasks for $filecount files"
rm -f "$tempdir"/worker*
concurrency=$(( maxload / 2 ))
(( concurrency )) || concurrency=1
active=0
for (( i=0 ; i < concurrency ; i++ ))
do
(( ++wnum ))
mkfifo "$tempdir"/worker${wnum}{in,out}
worker $wnum <"$tempdir"/worker${wnum}in >"$tempdir"/worker${wnum}out &
workers[wnum]=$!
eval exec $((100+wnum))'>"$tempdir"/worker${wnum}in'
eval exec $((200+wnum))'<"$tempdir"/worker${wnum}out'
done
concurrencychange=$(date +%s)
starttime=$concurrencychange
taskcount=$count
failed=0
while :
do
if read -n 1 -t 0.01 userinput
then
case $userinput in
'+')
((maxload++))
;;
'-')
((--maxload)) || ((maxload=1))
;;
[qQ])
quit=1
;;
esac
fi
read humanload garbage < /proc/loadavg
load=${humanload%.*}
if [ -z "$quit" ] && (( $(date +%s)-concurrencychange >= loadinterval ))
then
if (( concurrency > 1 )) \
&& (( load > maxload ))
then
concurrencychange=$(date +%s)
(( --concurrency ))
elif (( load < maxload ))
then
concurrencychange=$(date +%s)
(( ++concurrency ))
(( ++wnum ))
mkfifo "$tempdir"/worker${wnum}{in,out}
worker $wnum \
<"$tempdir"/worker${wnum}in \
>"$tempdir"/worker${wnum}out &
workers[wnum]=$!
eval exec $((100+wnum))'>"$tempdir"/worker${wnum}in'
eval exec $((200+wnum))'<"$tempdir"/worker${wnum}out'
fi
fi
master
if ((taskcount - remaining))
then
currenttime=$(date +%s)
secsremaining=$((
remaining
*
(currenttime - starttime)
/
(taskcount - remaining)
))
(( days =
secsremaining
/
( 24*60*60 )
)) || true
(( hours =
( secsremaining - ( days*24*60*60 ) )
/
( 60*60 )
)) || true
(( minutes =
( secsremaining - ( ( days*24 + hours ) *60*60 ) )
/
60
)) || true
fi
echo -en "\rload: $humanload / $maxload" \
"workers: $active / $concurrency" \
"done: $(( (taskcount - remaining ) * 100 / taskcount ))%" \
"- $((taskcount - remaining)) of $taskcount ($failed failed)" \
"${days}d ${hours}h${minutes}m "
done
endtime=$(date +%s)
(( elapsedseconds = endtime - starttime ))
(( days =
secsremaining
/
( 24*60*60 )
)) || true
(( hours =
( secsremaining - ( days*24*60*60 ) )
/
( 60*60 )
)) || true
(( minutes =
( secsremaining - ( ( days*24 + hours ) *60*60 ) )
/
60
)) || true
(( seconds =
secsremaining
-
( ( ( ( days*24 + hours ) *60 ) + minutes ) *60 )
)) || true
echo -e "\rRan $taskcount tasks, $failed of which failed, in $days" \
"days, $hours hours, $minutes minutes and $seconds seconds."
echo '
SELECT id,
filename
FROM destination_files
WHERE source_file_id is NULL;
SELECT "AtOM:NoMoreFiles";
' >&3
deleted=0
removed=0
read -u4 line
until [[ $line == AtOM:NoMoreFiles ]]
do
id=${line%|*}
filename=${line#*|}
if [ -n "$filename" ]
then
if rm -f "$filename"
then
Delete destination_files <<<"id = $id"
(( ++deleted ))
fi
else
Delete destination_files <<<"id = $id"
(( ++removed ))
fi
read -u4 line
done
echo "Suppressed $deleted files, $removed removed from database"
echo "Purging empty directories."
for path in "${destinationpath[@]}"
do
find "$path" -type d -empty -delete
done
closeDatabase
# vim:set ts=8 sw=8: