From 66e4b0c1a80db4dbc2cc3a939dcfe81635e5d75d Mon Sep 17 00:00:00 2001 From: Vincent Riquer Date: Sun, 7 Apr 2013 01:27:07 +0200 Subject: [PATCH] move eisting functions to separate files --- atom | 1604 +-------------------------- lib/config/getConfig | 27 + lib/config/getConfigDestination | 170 +++ lib/config/getConfigGeneral | 35 + lib/config/getConfigSource | 10 + lib/copy/checkCopy | 18 + lib/copy/copyFile | 28 + lib/database/Delete | 23 + lib/database/Insert | 37 + lib/database/InsertIfUnset | 42 + lib/database/InsertOrUpdate | 66 ++ lib/database/Select | 35 + lib/database/Update | 62 ++ lib/database/closeDatabase | 9 + lib/database/openDatabase | 27 + lib/decode/decodeFile | 48 + lib/decode/decodeMpcdec | 4 + lib/decode/decodeOpusdec | 4 + lib/decode/decodeSox | 30 + lib/destinations/createDestinations | 16 + lib/destinations/updateMimes | 31 + lib/encode/encodeFile::mp3 | 62 ++ lib/encode/encodeFile::opus | 38 + lib/encode/encodeFile::vorbis | 34 + lib/files/getDestDir | 34 + lib/files/getDestFile | 18 + lib/files/getFiles | 63 ++ lib/files/removeObsoleteFiles | 5 + lib/files/sanitizeFile | 23 + lib/tags/getInfos::APE | 54 + lib/tags/getInfos::FLAC | 44 + lib/tags/getInfos::MP3 | 28 + lib/tags/getInfos::Ogg | 29 + lib/tags/getInfos::Opus | 29 + lib/tags/getRateChannelMPC | 15 + lib/tags/getRateChannelSoxi | 4 + lib/tags/getTags | 41 + lib/tags/gettag | 4 + lib/tags/tryAPE | 5 + lib/tasks/gettaskinfos | 26 + lib/tools/progressSpin | 9 + lib/workers/checkworkers | 19 + lib/workers/cleaner | 64 ++ lib/workers/createworker | 4 + lib/workers/destroyworker | 5 + lib/workers/getworkerid | 14 + lib/workers/master | 153 +++ lib/workers/worker | 14 + 48 files changed, 1568 insertions(+), 1596 deletions(-) create mode 100644 lib/config/getConfig create mode 100644 lib/config/getConfigDestination create mode 100644 lib/config/getConfigGeneral create mode 100644 lib/config/getConfigSource create mode 100644 lib/copy/checkCopy create mode 100644 lib/copy/copyFile create mode 100644 lib/database/Delete create mode 100644 lib/database/Insert create mode 100644 lib/database/InsertIfUnset create mode 100644 lib/database/InsertOrUpdate create mode 100644 lib/database/Select create mode 100644 lib/database/Update create mode 100644 lib/database/closeDatabase create mode 100644 lib/database/openDatabase create mode 100644 lib/decode/decodeFile create mode 100644 lib/decode/decodeMpcdec create mode 100644 lib/decode/decodeOpusdec create mode 100644 lib/decode/decodeSox create mode 100644 lib/destinations/createDestinations create mode 100644 lib/destinations/updateMimes create mode 100644 lib/encode/encodeFile::mp3 create mode 100644 lib/encode/encodeFile::opus create mode 100644 lib/encode/encodeFile::vorbis create mode 100644 lib/files/getDestDir create mode 100644 lib/files/getDestFile create mode 100644 lib/files/getFiles create mode 100644 lib/files/removeObsoleteFiles create mode 100644 lib/files/sanitizeFile create mode 100644 lib/tags/getInfos::APE create mode 100644 lib/tags/getInfos::FLAC create mode 100644 lib/tags/getInfos::MP3 create mode 100644 lib/tags/getInfos::Ogg create mode 100644 lib/tags/getInfos::Opus create mode 100644 lib/tags/getRateChannelMPC create mode 100644 lib/tags/getRateChannelSoxi create mode 100644 lib/tags/getTags create mode 100644 lib/tags/gettag create mode 100644 lib/tags/tryAPE create mode 100644 lib/tasks/gettaskinfos create mode 100644 lib/tools/progressSpin create mode 100644 lib/workers/checkworkers create mode 100644 lib/workers/cleaner create mode 100644 lib/workers/createworker create mode 100644 lib/workers/destroyworker create mode 100644 lib/workers/getworkerid create mode 100644 lib/workers/master create mode 100644 lib/workers/worker diff --git a/atom b/atom index 6c9ae7b..fe2b3e2 100755 --- a/atom +++ b/atom @@ -35,6 +35,7 @@ declare -A \ declare -r \ DOCDIR=./doc \ + LIBDIR=./lib \ SHAREDIR=./share declare -r \ exampleconf=$DOCDIR/example.cfg \ @@ -44,9 +45,14 @@ declare -r \ LC_ALL=C +shopt -s extglob + source $SHAREDIR/id3genres -shopt -s extglob +for function in "$LIBDIR"/*/* +do + source "$function" +done #parse arguments OPTERR=0 @@ -85,1601 +91,7 @@ do 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 +#FIXME: check sanity if [ ! -f ~/.atom/atom.cfg ] then diff --git a/lib/config/getConfig b/lib/config/getConfig new file mode 100644 index 0000000..6c14e36 --- /dev/null +++ b/lib/config/getConfig @@ -0,0 +1,27 @@ +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 +} diff --git a/lib/config/getConfigDestination b/lib/config/getConfigDestination new file mode 100644 index 0000000..6195ed5 --- /dev/null +++ b/lib/config/getConfigDestination @@ -0,0 +1,170 @@ +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 +} diff --git a/lib/config/getConfigGeneral b/lib/config/getConfigGeneral new file mode 100644 index 0000000..125b7b6 --- /dev/null +++ b/lib/config/getConfigGeneral @@ -0,0 +1,35 @@ +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 +} diff --git a/lib/config/getConfigSource b/lib/config/getConfigSource new file mode 100644 index 0000000..9df19c3 --- /dev/null +++ b/lib/config/getConfigSource @@ -0,0 +1,10 @@ +getConfigSource() { + case "$key" in + 'path') + sourcepath="$value" + ;; + 'skip') + skippeddirectories+=( "$value" ) + ;; + esac +} diff --git a/lib/copy/checkCopy b/lib/copy/checkCopy new file mode 100644 index 0000000..7aa76d6 --- /dev/null +++ b/lib/copy/checkCopy @@ -0,0 +1,18 @@ +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} + )) + ) + ) +} diff --git a/lib/copy/copyFile b/lib/copy/copyFile new file mode 100644 index 0000000..9e7b784 --- /dev/null +++ b/lib/copy/copyFile @@ -0,0 +1,28 @@ +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 )) +} diff --git a/lib/database/Delete b/lib/database/Delete new file mode 100644 index 0000000..2cd5718 --- /dev/null +++ b/lib/database/Delete @@ -0,0 +1,23 @@ +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 +} diff --git a/lib/database/Insert b/lib/database/Insert new file mode 100644 index 0000000..1e0338b --- /dev/null +++ b/lib/database/Insert @@ -0,0 +1,37 @@ +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" + } +} diff --git a/lib/database/InsertIfUnset b/lib/database/InsertIfUnset new file mode 100644 index 0000000..c81ce33 --- /dev/null +++ b/lib/database/InsertIfUnset @@ -0,0 +1,42 @@ +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" +} diff --git a/lib/database/InsertOrUpdate b/lib/database/InsertOrUpdate new file mode 100644 index 0000000..2fdf7c2 --- /dev/null +++ b/lib/database/InsertOrUpdate @@ -0,0 +1,66 @@ +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" +} diff --git a/lib/database/Select b/lib/database/Select new file mode 100644 index 0000000..4a0ca7e --- /dev/null +++ b/lib/database/Select @@ -0,0 +1,35 @@ +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 +} diff --git a/lib/database/Update b/lib/database/Update new file mode 100644 index 0000000..2e5f1c5 --- /dev/null +++ b/lib/database/Update @@ -0,0 +1,62 @@ +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 +} diff --git a/lib/database/closeDatabase b/lib/database/closeDatabase new file mode 100644 index 0000000..91dccad --- /dev/null +++ b/lib/database/closeDatabase @@ -0,0 +1,9 @@ +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} +} diff --git a/lib/database/openDatabase b/lib/database/openDatabase new file mode 100644 index 0000000..1b4f970 --- /dev/null +++ b/lib/database/openDatabase @@ -0,0 +1,27 @@ +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 +} diff --git a/lib/decode/decodeFile b/lib/decode/decodeFile new file mode 100644 index 0000000..fb6591b --- /dev/null +++ b/lib/decode/decodeFile @@ -0,0 +1,48 @@ +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 +} diff --git a/lib/decode/decodeMpcdec b/lib/decode/decodeMpcdec new file mode 100644 index 0000000..e1ee72c --- /dev/null +++ b/lib/decode/decodeMpcdec @@ -0,0 +1,4 @@ +decodeMpcdec() { + tmpfile="${fileid}mpcdec" + commandline=(mpcdec "$sourcepath/$filename" "$tempdir/$tmpfile.wav") +} diff --git a/lib/decode/decodeOpusdec b/lib/decode/decodeOpusdec new file mode 100644 index 0000000..4721b89 --- /dev/null +++ b/lib/decode/decodeOpusdec @@ -0,0 +1,4 @@ +decodeOpusdec() { + tmpfile="${fileid}opusdec" + commandline=(opusdec "$sourcepath/$filename" "$tempdir/$tmpfile.wav") +} diff --git a/lib/decode/decodeSox b/lib/decode/decodeSox new file mode 100644 index 0000000..84c29c6 --- /dev/null +++ b/lib/decode/decodeSox @@ -0,0 +1,30 @@ +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") +} diff --git a/lib/destinations/createDestinations b/lib/destinations/createDestinations new file mode 100644 index 0000000..f2c2d51 --- /dev/null +++ b/lib/destinations/createDestinations @@ -0,0 +1,16 @@ +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 +} diff --git a/lib/destinations/updateMimes b/lib/destinations/updateMimes new file mode 100644 index 0000000..8fc433d --- /dev/null +++ b/lib/destinations/updateMimes @@ -0,0 +1,31 @@ +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 +} diff --git a/lib/encode/encodeFile::mp3 b/lib/encode/encodeFile::mp3 new file mode 100644 index 0000000..b49bd05 --- /dev/null +++ b/lib/encode/encodeFile::mp3 @@ -0,0 +1,62 @@ +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 +} diff --git a/lib/encode/encodeFile::opus b/lib/encode/encodeFile::opus new file mode 100644 index 0000000..50d8a1c --- /dev/null +++ b/lib/encode/encodeFile::opus @@ -0,0 +1,38 @@ +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 +} diff --git a/lib/encode/encodeFile::vorbis b/lib/encode/encodeFile::vorbis new file mode 100644 index 0000000..7ff4acd --- /dev/null +++ b/lib/encode/encodeFile::vorbis @@ -0,0 +1,34 @@ +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 +} diff --git a/lib/files/getDestDir b/lib/files/getDestDir new file mode 100644 index 0000000..4f0baae --- /dev/null +++ b/lib/files/getDestDir @@ -0,0 +1,34 @@ +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 +} diff --git a/lib/files/getDestFile b/lib/files/getDestFile new file mode 100644 index 0000000..14f4874 --- /dev/null +++ b/lib/files/getDestFile @@ -0,0 +1,18 @@ +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") +} diff --git a/lib/files/getFiles b/lib/files/getFiles new file mode 100644 index 0000000..977bc4a --- /dev/null +++ b/lib/files/getFiles @@ -0,0 +1,63 @@ +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 +} diff --git a/lib/files/removeObsoleteFiles b/lib/files/removeObsoleteFiles new file mode 100644 index 0000000..6cf0f54 --- /dev/null +++ b/lib/files/removeObsoleteFiles @@ -0,0 +1,5 @@ +removeObsoleteFiles() { + Delete source_files <<-EOWhere + last_seen < $scantime + EOWhere +} diff --git a/lib/files/sanitizeFile b/lib/files/sanitizeFile new file mode 100644 index 0000000..90a3ebc --- /dev/null +++ b/lib/files/sanitizeFile @@ -0,0 +1,23 @@ +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" +} diff --git a/lib/tags/getInfos::APE b/lib/tags/getInfos::APE new file mode 100644 index 0000000..181c668 --- /dev/null +++ b/lib/tags/getInfos::APE @@ -0,0 +1,54 @@ +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" +} diff --git a/lib/tags/getInfos::FLAC b/lib/tags/getInfos::FLAC new file mode 100644 index 0000000..04533fa --- /dev/null +++ b/lib/tags/getInfos::FLAC @@ -0,0 +1,44 @@ +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" + ) +} diff --git a/lib/tags/getInfos::MP3 b/lib/tags/getInfos::MP3 new file mode 100644 index 0000000..62290dc --- /dev/null +++ b/lib/tags/getInfos::MP3 @@ -0,0 +1,28 @@ +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} +} diff --git a/lib/tags/getInfos::Ogg b/lib/tags/getInfos::Ogg new file mode 100644 index 0000000..175fe00 --- /dev/null +++ b/lib/tags/getInfos::Ogg @@ -0,0 +1,29 @@ +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%%,*} +} diff --git a/lib/tags/getInfos::Opus b/lib/tags/getInfos::Opus new file mode 100644 index 0000000..61198ba --- /dev/null +++ b/lib/tags/getInfos::Opus @@ -0,0 +1,29 @@ +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%%.*} +} diff --git a/lib/tags/getRateChannelMPC b/lib/tags/getRateChannelMPC new file mode 100644 index 0000000..05fdbc9 --- /dev/null +++ b/lib/tags/getRateChannelMPC @@ -0,0 +1,15 @@ +getRateChannelMPC() { + while read key value garbage + do + case $key in + 'samplerate:') + rate=$value + ;; + 'channels:') + channels=$value + ;; + esac + done < <( + mpcdec "$sourcepath/$filename" -i 2>&1 + ) +} diff --git a/lib/tags/getRateChannelSoxi b/lib/tags/getRateChannelSoxi new file mode 100644 index 0000000..a8613cb --- /dev/null +++ b/lib/tags/getRateChannelSoxi @@ -0,0 +1,4 @@ +getRateChannelSoxi() { + rate=$(soxi -r "$sourcepath/$filename" 2>/dev/null) + channels=$(soxi -c "$sourcepath/$filename" 2>/dev/null) +} diff --git a/lib/tags/getTags b/lib/tags/getTags new file mode 100644 index 0000000..dbc08e3 --- /dev/null +++ b/lib/tags/getTags @@ -0,0 +1,41 @@ +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 +} diff --git a/lib/tags/gettag b/lib/tags/gettag new file mode 100644 index 0000000..d6f6a2d --- /dev/null +++ b/lib/tags/gettag @@ -0,0 +1,4 @@ +gettag() { + echo -e "$infos" \ + | sed -n "/^${1}=/I{s/^${1}=//I;p;q}" +} diff --git a/lib/tags/tryAPE b/lib/tags/tryAPE new file mode 100644 index 0000000..3cf2db2 --- /dev/null +++ b/lib/tags/tryAPE @@ -0,0 +1,5 @@ +tryAPE() { + grep -q 'APETAGEX' \ + "$sourcepath/$filename" \ + && type=APE +} diff --git a/lib/tasks/gettaskinfos b/lib/tasks/gettaskinfos new file mode 100644 index 0000000..a92fe91 --- /dev/null +++ b/lib/tasks/gettaskinfos @@ -0,0 +1,26 @@ +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#*|} +} diff --git a/lib/tools/progressSpin b/lib/tools/progressSpin new file mode 100644 index 0000000..ba41f8e --- /dev/null +++ b/lib/tools/progressSpin @@ -0,0 +1,9 @@ +progressSpin() { + case $(( ++count % 40 )) in + 0) echo -ne '\b|' ;; + 10) echo -ne '\b/' ;; + 20) echo -en '\b-' ;; + 30) echo -ne '\b\\' ;; + *) ;; + esac +} diff --git a/lib/workers/checkworkers b/lib/workers/checkworkers new file mode 100644 index 0000000..f8b711e --- /dev/null +++ b/lib/workers/checkworkers @@ -0,0 +1,19 @@ +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 +} diff --git a/lib/workers/cleaner b/lib/workers/cleaner new file mode 100644 index 0000000..c1bc581 --- /dev/null +++ b/lib/workers/cleaner @@ -0,0 +1,64 @@ +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 +} diff --git a/lib/workers/createworker b/lib/workers/createworker new file mode 100644 index 0000000..a2b955a --- /dev/null +++ b/lib/workers/createworker @@ -0,0 +1,4 @@ +createworker() { + worker $1 & + workers[$1]=$! +} diff --git a/lib/workers/destroyworker b/lib/workers/destroyworker new file mode 100644 index 0000000..707c574 --- /dev/null +++ b/lib/workers/destroyworker @@ -0,0 +1,5 @@ +destroyworker() { + dyingworker=${workers[$1]} + unset workers[$1] + wait $dyingworker +} diff --git a/lib/workers/getworkerid b/lib/workers/getworkerid new file mode 100644 index 0000000..0d7ddc3 --- /dev/null +++ b/lib/workers/getworkerid @@ -0,0 +1,14 @@ +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-- )) +} diff --git a/lib/workers/master b/lib/workers/master new file mode 100644 index 0000000..518dc72 --- /dev/null +++ b/lib/workers/master @@ -0,0 +1,153 @@ +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 +} diff --git a/lib/workers/worker b/lib/workers/worker new file mode 100644 index 0000000..4dd40b5 --- /dev/null +++ b/lib/workers/worker @@ -0,0 +1,14 @@ +worker() { + exec 2>>"$tempdir/worker$1.log" + (( debug >= 2 )) && echo "${cmd_arg[@]}" >&2 + "${cmd_arg[@]}" >/dev/null +} +createworker() { + worker $1 & + workers[$1]=$! +} +destroyworker() { + dyingworker=${workers[$1]} + unset workers[$1] + wait $dyingworker +}