AtOM/atom
2013-03-08 12:53:29 +01:00

1085 lines
20 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 \
destinationpath \
destinationquality \
destinationrename \
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"
#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"
;;
'id3charset')
sourceid3charset="$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
;;
'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
;;
'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"
;;
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
mkfifo "$tempdir"/sqlite.{in,out}
if [ ! -f "$database" ]
then
if [ ! -d "${database%/*}" ]
then
mkdir -p "${database%/*}"
fi
sqlite3 "$database" < $schema
fi
sqlite3 "$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+=","
if [[ $value == NULL ]]
then
insert_values+="NULL"
else
insert_values+='"'"${value//\"/\"\"}"'"'
fi
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)
set_statement="${set_statement}="'"'"${argument//\"/\"\"}"'"'
what=key
;;
esac
done
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 "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
}
getFiles() {
scantime=$(date +%s)
# 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 ))
else
Update source_files last_seen $scantime <<-EOWhere
filename = $filename
EOWhere
fi
case $(( ++count % 4 )) in
0) echo -ne '\r|' ;;
1) echo -ne '\r/' ;;
2) echo -en '\r-' ;;
3) echo -ne '\r\\' ;;
esac
done < <(
find "$sourcepath" -type f -printf "%T@ %s %P\n"
)
echo 'COMMIT;' >&3
echo -e "\r$count 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"
)
}
gettag() {
echo -e "$infos" \
| sed -n "/^${1}=/I{s/^${1}=//I;p}"
}
getInfos::MP3() {
infos=$(
soxi -a "$sourcepath/$filename" 2>/dev/null
)
album=$(gettag album)
artist=$(gettag artist)
genre=$(gettag genre)
title=$(gettag title)
tracknum=$(gettag tracknumber)
year=$(gettag year)
getRateChannelSoxi
[ -n "$album" \
-o -n "$artist" \
-o -n "$genre" \
-o -n "$title" \
-o -n "$tracknum" \
-o -n "$year" ] \
|| return 1
}
getInfos::Ogg() {
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)
channels=$(gettag channels)
[ -n "$album" \
-o -n "$albumartist" \
-o -n "$artist" \
-o -n "$composer" \
-o -n "$disc" \
-o -n "$genre" \
-o -n "$performer" \
-o -n "$title" \
-o -n "$tracknum" \
-o -n "$year" ] \
|| return 1
}
getInfos::FLAC() {
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"
)
[ -n "$album" \
-o -n "$albumartist" \
-o -n "$artist" \
-o -n "$composer" \
-o -n "$disc" \
-o -n "$genre" \
-o -n "$performer" \
-o -n "$title" \
-o -n "$tracknum" \
-o -n "$year" ] \
|| return 1
}
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.
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"
[ -n "$album" \
-o -n "$albumartist" \
-o -n "$artist" \
-o -n "$composer" \
-o -n "$disc" \
-o -n "$genre" \
-o -n "$performer" \
-o -n "$title" \
-o -n "$tracknum" \
-o -n "$year" ] \
|| return 1
}
tryAPE() {
grep -q 'APETAGEX' \
"$sourcepath/$filename" \
&& type=APE
}
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 || return 1
else
return 1
fi
}
encodeFile::MP3() {
#lame
:
}
encodeFile::Ogg() {
#oggenc
:
}
transcodeFile() {
#sox -> wav
for format in $targets["format"]
do
#encodeFile::$format
:
done
#signal and of encoding to parent
}
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
}
#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
{
cat <<EOF
General|Load|$maxload
|Load Interval|$loadinterval
|Temp Dir|$tempdir
|Database|$database
|Debug|$debug
Source|Path|$sourcepath
|ID3 Charset|$sourceid3charset
EOF
for destination in ${!destinationpath[@]}
do
cat <<EOF
$destination|Path|${destinationpath[$destination]}
|Format|${destinationformat[$destination]}
|Quality|${destinationquality[$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
}|column -t -s'|' -n
openDatabase
createDestinations
getFiles
updateMimes
removeObsoleteFiles
# get files
echo '
SELECT COUNT(DISTINCT source_files.filename)
FROM source_files,
destinations,
destination_files,
mime_type_actions,
tags
WHERE destination_files.last_change < source_files.last_change
AND destinations.id = destination_files.destination_id
AND mime_type_actions.destination_id = destinations.id
AND mime_type_actions.id = source_files.mime_type
AND source_files.id = destination_files.source_file_id
AND source_files.id = tags.source_file
AND tags.last_change < source_files.last_change
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,
destinations,
destination_files,
mime_type_actions,
tags
WHERE destination_files.last_change < source_files.last_change
AND destinations.id = destination_files.destination_id
AND mime_type_actions.destination_id = destinations.id
AND mime_type_actions.id = source_files.mime_type
AND source_files.id = destination_files.source_file_id
AND source_files.id = tags.source_file
AND tags.last_change < source_files.last_change
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 getTags
then
Update tags \
album "$album" \
albumartist "$albumartist" \
artist "$artist" \
composer "$composer" \
disc "$disc" \
genre "$genre" \
performer "$performer" \
title "$title" \
track "$tracknum" \
year "$year" \
last_change "$lastchange" \
rate "$rate" \
channels "$channels" \
>/dev/null <<<"source_file = $sourcefileid"
unset genre \
albumartist \
year \
album \
disc \
artist \
tracknum \
title \
composer \
performer \
rate \
channels
fi
done
echo 'COMMIT;' >&3
echo -e "\rRead tags from $count files."
unset count tagfiles
echo '
CREATE TEMPORARY TABLE tasks(
id INTEGER PRIMARY KEY,
command_line TEXT,
requires INTEGER,
status INTEGER NOT NULL,
FOREIGN KEY(requires) REFERENCES tasks(id)
ON DELETE SET NULL
);' >&3
closeDatabase
# vim:set ts=8 sw=8: