#!/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 \ SHAREDIR=./share declare -r \ exampleconf=$DOCDIR/example.cfg \ schema=$SHAREDIR/schema.sql \ \ oldIFS="$IFS" LC_ALL=C 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 ;; 'opus') destinationformat["$destination"]=opus ;; '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 'opus') destinationquality["$destination"]="$value" ;; 'mp3') destinationquality["$destination"]="$value" ;; *) echo "$Invalid parameter \"$key\" for format \"${destinationformat["$destination"]}\"" >&2 exit $EFMTINVPARM ;; esac ;; 'loss') expr='^[0-9]*$' if ! [[ $value =~ $expr ]] then echo "Invalid loss value: $value" >&2 exit $EQUALITY fi unset expr case "${destinationformat["$destination"]}" in 'opus') destinationloss["$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##*/}" ;; 'fat32compat') case $value in 'true'|'on'|'yes') destinationfat32compat["$destination"]=1 ;; 'false'|'off'|'no') destinationfat32compat["$destination"]=0 ;; *) echo "fat32compat takes values:" \ "'yes' ,'true' ,'on', 'no', 'false',"\ "'off'" ;; esac ;; '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") if [[ $mimetype == application/ogg ]] then case "$(head -n1 "$sourcepath/$filename")" in *'vorbis'*) mimetype+=' vorbis' ;; *'OpusHead'*) mimetype+=' opus' ;; esac fi 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-2' 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=${genre%)} genre=${genre#(} 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%%,*} } getInfosOpus_version='Opus-1' tagreaders+=( "$getInfosOpus_version" ) getInfos::Opus() { tagreader="$getInfosOpus_version" infos=$( opusinfo "$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 'original sample rate'|head -n1) channels=$(gettag channels|head -n1) bitrate=$(gettag 'average 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-2' tagreaders+=( "$getTags_version" ) getTags() { unset type case "$mimetype" in audio/mpeg) type=MP3 ;; 'application/ogg opus') type=Opus ;; 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() { commandline=(sox --single-threaded --temp "$tempdir") soxoptions_in='' soxoptions_out='' if (( ${destinationnormalize["$destination"]} )) then commandline+=(--norm) soxoptions_in+=' --norm' fi if [ -n "$1" ] then commandline+=("$1") else commandline+=("$sourcepath/$filename") fi if [ -n "${destinationfrequency["$destination"]}" ] \ && (( ${rate:-0} != ${destinationfrequency["$destination"]} )) then commandline+=(-r ${destinationfrequency["$destination"]}) soxoptions_out+=" -r ${destinationfrequency["$destination"]}" fi if [ -n "${destinationchannels["$destination"]}" ] \ && (( ${channels:-0} != ${destinationchannels["$destination"]} )) then commandline+=(-c ${destinationchannels["$destination"]}) soxoptions_out+=" -c ${destinationchannels["$destination"]}" fi tmpfile="$fileid${soxoptions_in// /}${soxoptions_out// /}" commandline+=("$tempdir/$tmpfile.wav") } decodeMpcdec() { tmpfile="${fileid}mpcdec" commandline=(mpcdec "$sourcepath/$filename" "$tempdir/$tmpfile.wav") } decodeOpusdec() { tmpfile="${fileid}opusdec" commandline=(opusdec "$sourcepath/$filename" "$tempdir/$tmpfile.wav") } decodeFile() { if ! decodetaskid=$( Select tasks id <<<"key = $tmpfile" ) then decodetaskid=$( Insert tasks <<-EOInsert key $tmpfile source_file $fileid $( for key in ${!commandline[@]} do echo "cmd_arg$key ${commandline[key]}" done ) 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 $( for key in ${!commandline[@]} do echo "cmd_arg$key ${commandline[key]}" done ) requires $decodetaskid required $decodetaskid status 0 cleanup $cleanup EOInsert ) progressSpin fi fi } copyFile() { extension="${filename##*.}" cp -al \ "$sourcepath/$filename" \ "$destdir/$destfile.$extension" \ 2>/dev/null \ || cp -a \ "$sourcepath/$filename" \ "$destdir/$destfile.$extension" echo \ "UPDATE destination_files" \ "SET filename=\"${filename//\"/\"\"}\"," \ " last_change=(" \ " SELECT last_change" \ " FROM source_files" \ " WHERE id=$fileid" \ " )," \ " old_filename=(" \ " SELECT filename" \ " FROM destination_files" \ " WHERE id=$destfileid" \ " )," \ " rename_pattern=" \ "\"${destinationrenamepath[$destination]}/${destinationrename[$destination]}:${destinationfat32compat["$destination"]}\""\ "WHERE id=$destfileid;" \ >&3 (( ++copies )) } sanitizeFile() { shopt -s extglob string="$1" # Filenames can't contain / string="${string//\// }" if (( ${destinationfat32compat[$destination]} )) then # Filenames can't contain: 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/%+( )/} 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}" replace=$(sanitizeFile "$disc") destdir="${destdir//%\{disc\}/$replace}" else destdir+=$(sanitizeFile "${filename%%/*}") part=${filename#*/} while [[ $part =~ / ]] do destdir+="/$(sanitizeFile "${part%%/*}")" part=${part#*/} done 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}" destfile="${destfile//%\{disc\}/$disc}" else destfile="${filename##*/}" destfile="${destfile%.*}" fi destfile=$(sanitizeFile "$destfile") } encodeFile::mp3() { lameopts=(lame --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 lameopts+=("$tempdir/$tmpfile.wav" "$destdir/$destfile.mp3") encodetaskid=$( Insert tasks <<-EOInsert key ${fileid}lame$destination requires ${soxtaskid:-$decodetaskid} required ${soxtaskid:-$decodetaskid} fileid $destfileid filename $destdir/$destfile.mp3 $( for key in ${!lameopts[@]} do echo "cmd_arg$key ${lameopts[key]}" done ) cleanup $tempdir/$tmpfile.wav source_file $fileid status 0 rename_pattern ${destinationrenamepath[$destination]}/${destinationrename[$destination]}:${destinationfat32compat["$destination"]} EOInsert ) progressSpin } encodeFile::opus() { opusencopts=(opusenc --music --quiet) opusencopts+=(--bitrate ${destinationquality[$destination]}) [ -n "${destinationloss["$destination"]}" ] \ && opusencopts+=(--expect-loss "${destinationloss["$destination"]}") [ -n "$albumartist" ] && opusencopts+=(--comment "ALBUMARTIST=$albumartist") [ -n "$album" ] && opusencopts+=(--comment "ALBUM=$album") [ -n "$artist" ] && opusencopts+=(--artist "$artist") [ -n "$composer" ] && opusencopts+=(--comment "COMPOSER=$composer") [ -n "$disc" ] && opusencopts+=(--comment "DISCNUMBER=$disc") [ -n "$genre" ] && opusencopts+=(--comment "GENRE=$genre") [ -n "$performer" ] && opusencopts+=(--comment "PERFORMER=$performer") [ -n "$title" ] && opusencopts+=(--title "$title") [ -n "$track" ] && opusencopts+=(--comment "TRACKNUMBER=${track%/*}") [ -n "${track#*/}" ] && opusencopts+=(--comment "TRACKTOTAL=${track#*/}") [ -n "$year" ] && opusencopts+=(--comment "DATE=$year") opusencopts+=("$tempdir/$tmpfile.wav" "$destdir/$destfile.opus") encodetaskid=$( Insert tasks <<-EOInsert key ${fileid}opusenc$destination requires ${soxtaskid:-$decodetaskid} required ${soxtaskid:-$decodetaskid} fileid $destfileid filename $destdir/$destfile.ogg $( for key in ${!opusencopts[@]} do echo "cmd_arg$key ${opusencopts[key]}" done ) cleanup $tempdir/$tmpfile.wav source_file $fileid status 0 rename_pattern ${destinationrenamepath[$destination]}/${destinationrename[$destination]}:${destinationfat32compat["$destination"]} EOInsert ) progressSpin } encodeFile::vorbis() { oggencopts=(oggenc -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") oggencopts+=(-o "$destdir/$destfile.ogg" "$tempdir/$tmpfile.wav") encodetaskid=$( Insert tasks <<-EOInsert key ${fileid}oggenc$destination requires ${soxtaskid:-$decodetaskid} required ${soxtaskid:-$decodetaskid} fileid $destfileid filename $destdir/$destfile.ogg $( for key in ${!oggencopts[@]} do echo "cmd_arg$key ${oggencopts[key]}" done ) cleanup $tempdir/$tmpfile.wav source_file $fileid status 0 rename_pattern ${destinationrenamepath[$destination]}/${destinationrename[$destination]}:${destinationfat32compat["$destination"]} EOInsert ) progressSpin } worker() { exec 2>>"$tempdir/worker$1.log" (( debug >= 2 )) && echo "${cmd_arg[@]}" >&2 "${cmd_arg[@]}" >/dev/null } master() { if (( active >= concurrency)) || [ -n "$quit" ] then sleep 0.1 else echo ' SELECT COUNT(*) FROM tasks WHERE status = 0; SELECT COUNT(*) FROM tasks WHERE status = 0 AND requires is NULL; SELECT id, source_file, required, cmd_arg0, cmd_arg1, cmd_arg2, cmd_arg3, cmd_arg4, cmd_arg5, cmd_arg6, cmd_arg7, cmd_arg8, cmd_arg9, cmd_arg10, cmd_arg11, cmd_arg12, cmd_arg13, cmd_arg14, cmd_arg15, cmd_arg16, cmd_arg17, cmd_arg18, cmd_arg19, cmd_arg20, cmd_arg21, cmd_arg22, cmd_arg23, cmd_arg24, cmd_arg25, cmd_arg26, cmd_arg27, cmd_arg28, cmd_arg29, 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 sleep 0.1 continue elif (( ready == 0 )) then sleep 0.1 else (( ++active )) read -u4 line taskid=${line%%|*} rest="${line#*|}|" sourcefileid=${rest%%|*} rest=${rest#*|} required=${rest%%|*} rest=${rest#*|} cmd_arg=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cmd_arg+=("${rest%%|*}") rest=${rest#*|} cleanup=${rest%%|*} rest=${rest#*|} destfileid=${rest%%|*} rest=${rest#*|} destfilename=${rest%%|*} rest=${rest#*|} for key in ${!cmd_arg[@]} do [ -z "${cmd_arg[key]}" ] && unset cmd_arg[key] done workerid=$(getworkerid) workertasks[workerid]=$taskid Update tasks status 1 <<<"id = $taskid" createworker $workerid fi fi } getworkerid() { local i for (( i=0 ; i >= 0 ; i++ )) do if [ -z "${workers[i]}" ] then echo $i return fi done # If we reach this, we have reached the signed long limit # (2^63 - 1 = 9223372036854775807 - Got a supercomputer?) (( concurrency-- )) } createworker() { worker $1 & workers[$1]=$! } destroyworker() { dyingworker=${workers[$1]} unset workers[$1] wait $dyingworker } gettaskinfos() { echo ' SELECT id, source_file, required, cleanup, fileid, filename FROM tasks WHERE id='$1'; ' >&3 read -u4 line taskid=${line%%|*} rest="${line#*|}|" sourcefileid=${rest%%|*} rest=${rest#*|} required=${rest%%|*} rest=${rest#*|} cleanup=${rest%%|*} rest=${rest#*|} destfileid=${rest%%|*} rest=${rest#*|} destfilename=${rest%%|*} rest=${rest#*|} } cleaner() { for key in ${!failedtasks[@]} do taskid=${failedtasks[key]} gettaskinfos $taskid faildepends=$( Select tasks 'COUNT(*)' <<-EOWhere requires = $taskid EOWhere ) (( failed+=faildepends )) Update tasks status 2 <<<"id = $taskid" Update tasks status 2 <<<"requires = $taskid" echo "SELECT COUNT(*) FROM tasks WHERE ( status = 0 OR status = 1 ) AND required = $taskid;">&3 read -u4 count if (( count == 0 )) then rm -f "$cleanup" fi unset failedtasks[key] done for key in ${!finishedtasks[@]} do taskid=${finishedtasks[key]} gettaskinfos $taskid if [ -n "$destfilename" ] then echo \ "UPDATE destination_files" \ "SET filename=\"${destfilename//\"/\"\"}\"," \ " last_change=(" \ " SELECT last_change" \ " FROM source_files" \ " WHERE id=$sourcefileid" \ " )," \ " old_filename=(" \ " SELECT filename" \ " FROM destination_files" \ " WHERE id=$destfileid" \ " )," \ " rename_pattern=(" \ " SELECT rename_pattern" \ " FROM tasks" \ " WHERE id=$taskid" \ " )" \ "WHERE id=$destfileid;" \ >&3 fi echo "SELECT COUNT(*) FROM tasks WHERE ( status = 0 OR status = 1 ) AND required = $taskid;">&3 read -u4 count if (( count == 0 )) then rm -f "$cleanup" fi Delete tasks <<<"id = $taskid" unset finishedtasks[key] done } checkworkers() { for key in ${!workers[@]} do if ! kill -0 ${workers[key]} 2>/dev/null then taskid=${workertasks[key]} (( ++ran )) (( active-- )) if destroyworker $key then finishedtasks+=($taskid) else failedtasks+=($taskid) (( ++failed )) fi unset workertasks[key] fi done } #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 +H 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 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 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 (${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 )) 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: %2.1f/%i' fmtworkers='W: %i/%i' fmtprogress="T:%${#taskcount}i/%i (F:%i) %3i%%" fmttime='%2id %2ih%02im%02is (A:%2.1fs/task)' printf \ "\r$fmtload $fmtworkers $fmtprogress $fmttime" \ $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." 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 $'\r'"$destination: Renamed ${count:-0} files " fi unset count done 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: