AtOM/atom
2013-04-09 22:55:31 +02:00

804 lines
16 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 \
destinationfat32compat \
destinationcopymime \
destinationformat \
destinationfrequency \
destinationid \
destinationloss \
destinationmaxbps \
destinationnormalize \
destinationpath \
destinationquality \
destinationrename \
destinationnoresample \
destinationrenamepath \
destinationskipmime \
|| {
echo "Check your Bash version. You need >= 4.0" >&2
exit $EBASHVERS
}
declare -r \
DOCDIR=./doc \
LIBDIR=./lib \
SHAREDIR=./share
declare -r \
exampleconf=$DOCDIR/example.cfg \
schema=$SHAREDIR/schema.sql \
\
oldIFS="$IFS"
cffile="$HOME/.atom/atom.cfg"
LC_ALL=C
shopt -s extglob
source $SHAREDIR/id3genres
for function in "$LIBDIR"/*/*
do
source "$function"
done
#parse arguments
OPTERR=0
while getopts ':c:Cl:T:F:hD' opt
do
case $opt in
c)
cffile="$OPTARG"
;;
C)
cfgdump=1
;;
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
#FIXME: check sanity
if [ ! -f "$cffile" ]
then
if [ ! -d ~/.atom ]
then
mkdir -p ~/.atom
fi
sed "s:%HOME%:$HOME:" "$exampleconf" > "$cffile"
cat >&2 <<-EOCfgNotice
No configuration file found!
An example file has been created as "${cffile/$HOME/~}".
You should change it to your likings using you favorite editor.
Bailing out.
EOCfgNotice
exit $ENOCFG
fi
getConfig
set +H
(( debug || cfgdump )) && printConfig
(( cfgdump )) && exit
openDatabase
createDestinations
getFiles
updateMimes
removeObsoleteFiles
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"
# 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,
rename_pattern TEXT,
source_file INTEGER,
fileid INTEGER,
filename TEXT,
cmd_arg0 TEXT,
cmd_arg1 TEXT,
cmd_arg2 TEXT,
cmd_arg3 TEXT,
cmd_arg4 TEXT,
cmd_arg5 TEXT,
cmd_arg6 TEXT,
cmd_arg7 TEXT,
cmd_arg8 TEXT,
cmd_arg9 TEXT,
cmd_arg10 TEXT,
cmd_arg11 TEXT,
cmd_arg12 TEXT,
cmd_arg13 TEXT,
cmd_arg14 TEXT,
cmd_arg15 TEXT,
cmd_arg16 TEXT,
cmd_arg17 TEXT,
cmd_arg18 TEXT,
cmd_arg19 TEXT,
cmd_arg20 TEXT,
cmd_arg21 TEXT,
cmd_arg22 TEXT,
cmd_arg23 TEXT,
cmd_arg24 TEXT,
cmd_arg25 TEXT,
cmd_arg26 TEXT,
cmd_arg27 TEXT,
cmd_arg28 TEXT,
cmd_arg29 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 opus')
if [[ ${destinationformat[$destination]} = opus ]] \
&& checkCopy
then
copied=1
else
decodeOpusdec
if (( ${destinationnormalize["$destination"]}))\
|| (
[ -n "${destinationfrequency["$destination"]}" ]\
&& (( ${rate:-0} != ${destinationfrequency["$destination"]}))\
) || (
[ -n "${destinationchannels["$destination"]}" ]\
&& (( ${channels:-0} != ${destinationchannels["$destination"]} ))
)
then
sox_needed=1
fi
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
copyFiles_matching
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 (${copies:-0} immediate copies)"
concurrency=$(( maxload / 2 ))
(( concurrency )) || concurrency=1
active=0
concurrencychange=$(date +%s)
starttime=$concurrencychange
taskcount=$count
remaining=$taskcount
failed=0
while (( (remaining || ${#workers[@]}) && ! quit ))
do
if read -n 1 -t 0.1 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 )) && (( active > concurrency - 1 ))
then
concurrencychange=$(date +%s)
(( ++concurrency ))
fi
fi
checkworkers
cleaner
master
if (( ran ))
then
currenttime=$(date +%s)
avgduration=$((
((currenttime - starttime) * 1000)
/
ran
))
secsremaining=$(( remaining * avgduration / 1000 ))
(( 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
avgduration=$(printf %04i $avgduration)
avgdsec=${avgduration:0:-3}
avgdmsec=${avgduration#$avgdsec}
fi
fmtload='L:%4.1f/%i'
fmtworkers='W:%i/%i'
fmtprogress="T:%${#taskcount}i/%i (F:%i) %3i%%"
fmttime='%2id %2ih%02im%02is (A:%4.1fs/task)'
eta="ETA:$(
date -d "${days:-0} days
${hours:-0} hours
${minutes:-0} minutes
${seconds:-0} seconds" \
+'%d/%m %H:%M:%S'
)"
printf \
"\r$fmtload $fmtworkers $fmtprogress $fmttime $eta\033[K"\
$humanload \
$maxload \
${active:-0} \
${concurrency:-0} \
${ran:-0} \
${taskcount:-0} \
${failed:-0} \
$(( ran * 100 / taskcount )) \
${days:-0} \
${hours:-0} \
${minutes:-0} \
${seconds:-0} \
${avgdsec:-0}.${avgdmsec:-0}
done
unset count
endtime=$(date +%s)
(( elapsedseconds = endtime - starttime ))
(( days =
elapsedseconds
/
( 24*60*60 )
)) || true
(( hours =
( elapsedseconds - ( days*24*60*60 ) )
/
( 60*60 )
)) || true
(( minutes =
( elapsedseconds - ( ( days*24 + hours ) *60*60 ) )
/
60
)) || true
(( seconds =
elapsedseconds
-
( ( ( ( days*24 + hours ) *60 ) + minutes ) *60 )
)) || true
echo -e "\rRan ${ran:=0} tasks, $failed of which failed, in $days" \
"days, $hours hours, $minutes minutes and $seconds seconds.\033[K"
if [ -n "$quit" ]
then
closeDatabase
exit
fi
#set -x
for destination in "${!destinationpath[@]}"
do
echo '
SELECT
destination_files.filename,
destination_files.id,
source_files.filename,
tags.album,
tags.albumartist,
tags.artist,
tags.composer,
tags.disc,
tags.genre,
tags.performer,
tags.title,
tags.track,
tags.year
FROM destination_files
INNER JOIN destinations
ON destination_files.destination_id
=destinations.id
INNER JOIN tags
ON destination_files.source_file_id
=tags.source_file
INNER JOIN source_files
ON destination_files.source_file_id
=source_files.id
WHERE destinations.name="'"$destination"'"
AND (destination_files.rename_pattern
!=
"'"${destinationrenamepath[$destination]}/${destinationrename[$destination]}:${destinationfat32compat["$destination"]}"'"
OR destination_files.rename_pattern is NULL)
AND destination_files.last_change > 0
;
SELECT "AtOM:NoMoreFiles";
' >&3
read -u4 line
if [[ $line != AtOM:NoMoreFiles ]]
then
case "${destinationformat[$destination]}" in
'mp3') extension=mp3 ;;
'opus') extension=opus ;;
'vorbis') extension=ogg ;;
esac
echo -n "$destination: rename pattern changed, renaming files... "
while [[ $line != AtOM:NoMoreFiles ]]
do
oldfilename=${line%%|*}
rest=${line#*|}'|'
destfileid=${rest%%|*}
rest=${rest#*|}
filename=${rest%%|*}
rest=${rest#*|}
album=${rest%%|*}
rest=${rest#*|}
albumartist=${rest%%|*}
rest=${rest#*|}
artist=${rest%%|*}
rest=${rest#*|}
composer=${rest%%|*}
rest=${rest#*|}
disc=${rest%%|*}
rest=${rest#*|}
genre=${rest%%|*}
rest=${rest#*|}
performer=${rest%%|*}
rest=${rest#*|}
title=${rest%%|*}
rest=${rest#*|}
track=${rest%%|*}
rest=${rest#*|}
year=${rest%%|*}
rest=${rest#*|}
if [ -n "$oldfilename" -a -f "$oldfilename" ]
then
getDestDir
getDestFile
destfilename="$destdir/$destfile.$extension"
if [[ $oldfilename != $destfilename ]]
then
mv "$oldfilename" "$destfilename"
progressSpin
fi
echo "UPDATE destination_files" \
"SET filename=\"${destfilename//\"/\"\"}\"," \
" rename_pattern=" \
"\"${destinationrenamepath[$destination]}/${destinationrename[$destination]}:${destinationfat32compat["$destination"]}\"" \
"WHERE id=$destfileid;" \
>&3
fi
read -u4 line
done
echo -e $'\r'"$destination: Renamed ${count:-0} files\033[K"
fi
unset count
done
copyFiles_action
echo '
SELECT id,
old_filename
FROM destination_files
WHERE old_filename IS NOT NULL;
SELECT "AtOM:NoMoreFiles";
' >&3
echo "Removing obsolete files... "
read -u4 line
while [[ $line != AtOM:NoMoreFiles ]]
do
id=${line%%|*}
filename=${line#*|}
if [ -f "$filename" ]
then
rm -f "$filename"
fi
Update destination_files old_filename NULL <<<"id = $id"
progressSpin
read -u4 line
done
echo $'\r'"Removed ${count:-0} obsolete files."
echo "Purging empty directories."
for path in "${destinationpath[@]}"
do
find "$path" -type d -empty -delete
done
closeDatabase
# vim:set ts=8 sw=8: