#!/bin/bash ## Define exit codes # General config errors [10-19] ELOAD=10 EINTERVAL=11 ENOCFG=19 # Source cofig errors [20-29] # Destination config errors [30-49] EFORMAT=30 ECHANNEL=31 EFMTINVPARM=49 # config structures declare -A \ destinationchannels \ destinationcopymime \ destinationformat \ destinationfrequency \ destinationid \ destinationpath \ destinationquality \ destinationrename \ destinationrenamepath \ destinationskipmime \ || { echo "Check your Bash version. You need >= 4.0" >&2 exit $EBASHVERS } declare -r \ DOCDIR=./doc \ SHAREDIR=./share declare -r \ exampleconf=$DOCDIR/example.cfg \ schema=$SHAREDIR/schema.sql \ \ oldIFS="$IFS" #parse arguments OPTERR=0 while getopts ':c:l:T:F:hD' opt do case $opt in c) cffile="$OPTARG" ;; l) cliload="$OPTARG" ;; T) cliltimer="$OPTARG" ;; F) forceall+=("$OPTARG") ;; h) help exit 0 ;; D) (( debug++ )) ;; :) echo "-$OPTARG requires an argument" help exit 127 ;; *) echo "Unrecognized option: -$OPTARG" help exit 127 ;; esac done #parse config getConfigGeneral() { case $key in 'max-load') expr='^[0-9]*$' if [[ $value =~ $expr ]] then maxload="$value" else echo "Invalid max-load value: $value" >&2 exit $ELOAD fi unset expr ;; 'load-interval') expr='^[0-9]*$' if [[ $value =~ $expr ]] then loadinterval="$value" else echo "Invalid load-interval value: $value" >&2 exit $EINTERVAL fi unset expr ;; 'temporary-directory') tempdir="$value" ;; 'database') database="$value" ;; debug) (( value > debug )) && debug=$value ;; esac } getConfigSource() { case "$key" in 'path') sourcepath="$value" ;; 'id3charset') sourceid3charset="$value" ;; esac } getConfigDestination() { case "$key" in 'path') destinationpath["$destination"]="$value" ;; 'format') case "$value" in 'mp3') destinationformat["$destination"]=mp3 ;; 'vorbis') destinationformat["$destination"]=vorbis ;; *) echo "Unsupported destination format: $value" >2& exit $EFORMAT ;; esac ;; 'quality') expr='^[0-9]*$' if ! [[ $value =~ $expr ]] then echo "Invalid quality value: $value" >&2 exit $EQUALITY fi unset expr case "${destinationformat["$destination"]}" in 'vorbis') destinationquality["$destination"]="$value" ;; *) echo "$Invalid parameter \"$key\" for format \"${destinationformat["$destination"]}\"" >&2 exit $EFMTINVPARM ;; esac ;; 'bitrate') expr='^[0-9]*$' if ! [[ $value =~ $expr ]] then echo "Invalid bitrate value: $value" >&2 exit $EQUALITY fi unset expr case "${destinationformat["$destination"]}" in 'mp3') destinationquality["$destination"]="$value" ;; *) echo "$Invalid parameter \"$key\" for format \"${destinationformat["$destination"]}\"" >&2 exit $EFMTINVPARM ;; esac ;; 'channels') expr='^[0-9]*$' if ! [[ $value =~ $expr ]] then echo "Invalid channel count: $value" >&2 exit $ECHANNEL fi unset expr destinationchannels["$destination"]=$value ;; 'frequency') expr='^[0-9]*$' if ! [[ $value =~ $expr ]] then echo "Invalid frequency value: $value" >&2 exit $ECHANNEL fi unset expr destinationfrequency["$destination"]=$value ;; 'rename') case "$value" in */*) destinationrenamepath["$destination"]="${value%/*}" ;; esac destinationrename["$destination"]="${value##*/}" ;; 'skip_mime-type') destinationskipmime[$destination]="${destinationskipmime[$destination]:+${destinationskipmime[$destination]}|}$value" ;; 'copy_mime-type') destinationcopymime[$destination]="${destinationcopymime[$destination]:+${destinationcopymime[$destination]}|}$value" ;; esac } getConfig() { while read key value do case $key in '#'*) #comment ;; '') #empty line ;; '[general]') context=General ;; '[source]') context=Source ;; \[*\]) context=Destination destination="${key#[}" destination="${destination%]}" ;; *) getConfig$context ;; esac done < ~/.atom/atom.cfg } #check sanity openDatabase() { if [ ! -d "$tempdir" ] then mkdir -p "$tempdir" fi mkfifo "$tempdir"/sqlite.{in,out} if [ ! -f "$database" ] then if [ ! -d "${database%/*}" ] then mkdir -p "${database%/*}" fi sqlite3 "$database" < $schema fi sqlite3 "$database" \ < "$tempdir/sqlite.in" \ > "$tempdir/sqlite.out" & exec 3> "$tempdir"/sqlite.in exec 4< "$tempdir"/sqlite.out if (( debug > 2 )) then exec 5>&3 exec 3> >(tee -a $tempdir/debug.log >&5) fi echo 'PRAGMA foreign_keys = ON;' >&3 } closeDatabase() { echo .quit >&3 (( debug )) && echo -n "Waiting for SQLite to terminate... " wait (( debug )) && echo OK exec 3>&- exec 4<&- rm "$tempdir"/sqlite.{in,out} } Select() { #Select table [col1 [col2 [..]]] < WHERE_key WHERE_operator WHERE_value # [WHERE_key WHERE_operator WHERE_value # […]] local \ table="$1" \ col \ columns \ operator \ results \ where_statement shift for col do (( ${#columns} )) && columns+=',' columns+="$col" done while read key operator value do (( ${#where_statement} )) && where_statement+=( "AND" ) where_statement+=( "$key $operator "'"'"${value//\"/\"\"}"'"' ) done echo "SELECT IFNULL(" \ "(SELECT $columns FROM $table" \ "WHERE ${where_statement[@]})" \ ",'SQL::Select:not found'" \ ");" >&3 read -u 4 results if ! [[ $results == "SQL::Select:not found" ]] then echo "$results" else return 1 fi } Insert() { #Insert table [no_id] < key value # [key value # […]] local \ table="$1" \ no_id="${2:-0}" \ insert_keys \ insert_values \ results while read key value do (( ${#insert_keys} )) && insert_keys+="," insert_keys+='`'"$key"'`' (( ${#insert_values} )) && insert_values+="," if [[ $value == NULL ]] then insert_values+="NULL" else insert_values+='"'"${value//\"/\"\"}"'"' fi done echo "INSERT INTO $table" \ "( $insert_keys )" \ "VALUES" \ "( $insert_values );" >&3 (( no_id )) || { echo 'SELECT LAST_INSERT_ROWID();' >&3 read -u 4 results echo "$results" } } Update(){ #Update table set_key set_value [set_key set_value […]] < where_key where_operator where_value # [where_key where_operator where_value # […]] local \ table="$1" \ key \ argument \ operator \ value \ set_statement \ where_keys \ where_values \ what \ where_statement \ results shift what=key for argument do case $what in key) set_statement="${set_statement:+${set_statement},}\`$argument\`" what=value ;; value) set_statement="${set_statement}="'"'"${argument//\"/\"\"}"'"' what=key ;; esac done while read key operator value do (( ${#where_statement} )) && where_statement+=( "AND" ) if [[ $value == NULL ]] then where_statement+=( "$key is NULL" ) else where_statement+=( "$key $operator "'"'"${value//\"/\"\"}"'"' ) fi done echo "UPDATE '$table' SET" \ "$set_statement" \ "WHERE" \ "${where_statement[@]}" \ ";" >&3 } InsertIfUnset() { #InsertIfUnset table [no_id] < key value \n key value local \ table="$1" \ no_id="${2:-0}" \ column \ key \ keys \ results \ value \ values while read key value do keys+=( "$key" ) values+=( "$value" ) done if (( no_id )) then column="${keys[0]}" else column='id' fi if ! results=$( Select "$table" "$column" < <( for key in ${!keys[@]} do echo "${keys[$key]}" = "${values[$key]}" done ) ) then results=$( Insert "$table" < <( for key in ${!keys[@]} do echo "${keys[$key]}" "${values[$key]}" done ) ) fi echo "$results" } InsertOrUpdate() { #InsertOrUpdate table set_key set_value [set_key set_value […]] < where_key where_value # [where_key where_value # […]] local \ table="$1" \ argument \ key \ keys \ set_keys \ set_values \ value \ values \ what \ results shift what=key for argument do case $what in key) set_keys+=( "$argument" ) what=value ;; value) set_values+=( "$argument" ) what=key ;; esac done while read key value do keys+=( "$key" ) values+=( "$value" ) done if results=$( Select "$table" ${keys[0]} < <( for key in ${!keys[@]} do echo "${keys[$key]}" = "${values[$key]}" done ) ) then Update "$table" "$@" < <( for key in ${!keys[@]} do echo "${keys[$key]}" = "${values[$key]}" done ) else results=$( Insert "$table" < <( for key in ${!set_keys[@]} do echo "${set_keys[$key]}" "${set_values[$key]}" done for key in ${!keys[@]} do echo "${keys[$key]}" "${values[$key]}" done ) ) fi echo "$results" } Delete() { #Delete table < where_key where_operator where_value # [where_key where_operator where_value # […]] local \ table="$1" \ key \ operator \ value \ where_statement \ results while read key operator value do (( ${#where_statement} )) && where_statement+=( "AND" ) if [[ $value == NULL ]] then where_statement+=( "$key is NULL" ) else where_statement+=( "$key $operator "'"'"${value//\"/\"\"}"'"' ) fi done echo "DELETE from $table WHERE ${where_statement[@]};" >&3 } createDestinations() { for destination in ${!destinationpath[@]} do if ! [ -d "${destinationpath["$destination"]}" ] then if ! mkdir -p "${destinationpath["$destination"]}" then echo "$destination: Could not create ${destinationpath["$destination"]}!" exit $EINVDEST fi fi destinationid["$destination"]=$( InsertIfUnset destinations <<<"name $destination" ) done } getFiles() { scantime=$(date +%s) # We probably have thousands of files, don't waste time on disk writes echo 'BEGIN TRANSACTION;' >&3 while read time size filename do if ! Select source_files id >/dev/null <<-EOWhere filename = $filename mime_type > 0 last_change >= $time EOWhere then mimetype=$(file -b --mime-type "$sourcepath/$filename") mimetypeid=$( InsertIfUnset mime_types <<-EOInsert mime_text $mimetype EOInsert ) InsertOrUpdate source_files \ last_change $time \ size $size \ last_seen $scantime \ mime_type $mimetypeid \ >/dev/null \ <<-EOWhere filename $filename EOWhere (( ++new )) else Update source_files last_seen $scantime <<-EOWhere filename = $filename EOWhere fi case $(( ++count % 4 )) in 0) echo -ne '\r|' ;; 1) echo -ne '\r/' ;; 2) echo -en '\r-' ;; 3) echo -ne '\r\\' ;; esac done < <( find "$sourcepath" -type f -printf "%T@ %s %P\n" ) echo 'COMMIT;' >&3 echo -e "\r$count files found, ${new:=0} new or changed." unset count } updateMimes() { Update mime_actions action 1 <<<"action != 1" for destination in ${!destinationskipmime[@]} do IFS='|' for mime_type in ${destinationskipmime["$destination"]} do IFS="$oldIFS" Update mime_type_actions action 0 >/dev/null < <( cat <<-EOWhere destination_id = ${destinationid["$destination"]} mime_text LIKE ${mime_type//\*/%} EOWhere ) done done for destination in ${!destinationcopymime[@]} do IFS='|' for mime_type in ${destinationcopymime["$destination"]} do IFS="$oldIFS" Update mime_type_actions action 2 >/dev/null < <( cat <<-EOWhere destination_id = ${destinationid["$destination"]} mime_text LIKE ${mime_type//\*/%} EOWhere ) done done } removeObsoleteFiles() { Delete source_files <<-EOWhere last_seen < $scantime EOWhere } getRateChannelSoxi() { rate=$(soxi -r "$sourcepath/$filename" 2>/dev/null) channels=$(soxi -c "$sourcepath/$filename" 2>/dev/null) } getRateChannelMPC() { while read key value garbage do case $key in 'samplerate:') rate=$value ;; 'channels:') channels=$value ;; esac done < <( mpcdec "$sourcepath/$filename" ) } gettag() { echo -e "$infos" \ | sed -n "/^${1}=/I{s/^${1}=//I;p}" } getInfos::MP3() { infos=$( soxi -a "$sourcepath/$filename" 2>/dev/null ) album=$(gettag album) artist=$(gettag artist) genre=$(gettag genre) title=$(gettag title) tracknum=$(gettag tracknumber) year=$(gettag year) getRateChannelSoxi [ -n "$album" \ -o -n "$artist" \ -o -n "$genre" \ -o -n "$title" \ -o -n "$tracknum" \ -o -n "$year" ] \ || return 1 } getInfos::Ogg() { infos=$( ogginfo "$sourcepath/$filename" \ | sed 's/\t//' ) albumartist=$(gettag albumartist) album=$(gettag album) artist=$(gettag artist) composer=$(gettag composer) disc=$(gettag discnumber) genre=$(gettag genre) performer=$(gettag performer) title=$(gettag title) tracknum=$(gettag tracknumber) tracktotal=$(gettag tracktotal) if [ -n "$tracknum" -a -n "$tracktotal" ] then tracknum="$tracknum/$tracktotal" fi year=$(gettag date) infos="${infos//: /=}" rate=$(gettag rate) channels=$(gettag channels) [ -n "$album" \ -o -n "$albumartist" \ -o -n "$artist" \ -o -n "$composer" \ -o -n "$disc" \ -o -n "$genre" \ -o -n "$performer" \ -o -n "$title" \ -o -n "$tracknum" \ -o -n "$year" ] \ || return 1 } getInfos::FLAC() { infos=$( metaflac \ --show-tag=ALBUM \ --show-tag=ALBUMARTIST \ --show-tag=ARTIST \ --show-tag=COMPOSER \ --show-tag=DATE \ --show-tag=DISCNUMBER \ --show-tag=GENRE \ --show-tag=PERFORMER \ --show-tag=TITLE \ --show-tag=TRACKNUMBER \ --show-tag=TRACKTOTAL \ "$sourcepath/$filename" ) albumartist=$(gettag albumartist) album=$(gettag album) artist=$(gettag artist) composer=$(gettag composer) disc=$(gettag discnumber) genre=$(gettag genre) performer=$(gettag performer) title=$(gettag title) tracknum="$(gettag tracknumber)/$(gettag tracktotal)" year=$(gettag date) if [ -n "$tracknum" -a -n "$tracktotal" ] then tracknum="$tracknum/$tracktotal" fi year=$(gettag DATE) { read rate read channels } < <( metaflac \ --show-sample-rate \ --show-channels \ "$sourcepath/$filename" ) [ -n "$album" \ -o -n "$albumartist" \ -o -n "$artist" \ -o -n "$composer" \ -o -n "$disc" \ -o -n "$genre" \ -o -n "$performer" \ -o -n "$title" \ -o -n "$tracknum" \ -o -n "$year" ] \ || return 1 } getInfos::APE() { # I was not able to find a decent cli tool to read APE tags. # This is raw but works for the very few MusePack files I got. # # Please tell me if you know of any good tool. IFS='=' while read tag value do IFS="$oldIFS" case $tag in [Aa][Ll][Bb][Uu][Mm]' '[Aa][Rr][Tt][Ii][Ss][Tt]) albumartist="$value" ;; [Aa][Rr][Tt][Ii][Ss][Tt]) artist="$value" ;; [Yy][Ee][Aa][Rr]) year="$value" ;; [Aa][Ll][Bb][Uu][Mm]) album="$value" ;; [Tt][Ii][Tt][Ll][Ee]) title="$value" ;; [Tt][Rr][Aa][Cc][Kk]) tracknum="$value" ;; [Gg][Ee][Nn][Rr][Ee]) genre="$value" ;; [Cc][Oo][Mm][Pp][Oo][Ss][Ee][Rr]) composer="$value" ;; [Pp][Ee][Rr][Ff][Oo][Rr][Mm][Ee][Rr]) performer="$value" ;; *) ;; esac IFS='=' done < <( IFS="$oldIFS" sed \ 's/APETAGEX/\n/;s/[\x00\-\x1F]\x00\+/\n/g;s/\x00/=/g' \ "$sourcepath/$filename" \ | egrep -i \ '^(Album Artist|Artist|Year|Album|Title|Track|Genre|Composer|Performer)=' ) IFS="$oldIFS" [ -n "$album" \ -o -n "$albumartist" \ -o -n "$artist" \ -o -n "$composer" \ -o -n "$disc" \ -o -n "$genre" \ -o -n "$performer" \ -o -n "$title" \ -o -n "$tracknum" \ -o -n "$year" ] \ || return 1 } tryAPE() { grep -q 'APETAGEX' \ "$sourcepath/$filename" \ && type=APE } getTags() { unset type case "$mimetype" in audio/mpeg) type=MP3 ;; application/ogg) type=Ogg ;; audio/x-flac) type=FLAC ;; *) extendedtype=$(file -b "$sourcepath/$filename") case "$extendedtype" in *' ID3 '*) type=MP3 ;; *' Musepack '*) getRateChannelMPC tryAPE ;; *) getRateChannelSoxi tryAPE ;; esac ;; esac if [ -n "$type" ] then getInfos::$type || return 1 else return 1 fi } encodeFile::MP3() { #lame : } encodeFile::Ogg() { #oggenc : } transcodeFile() { #sox -> wav for format in $targets["format"] do #encodeFile::$format : done #signal and of encoding to parent } checkFinished() { #retrieve info from finished transcodeFile() #update counters / metadata : } checkLoad() { #if load > threshold # decrease concurrency #elif load < threshold # increase concurrency #fi : } readUserInput() { #read + / - / q(uit) / p(ause) #initiate shutdown / change load threshold / SIGSTOP all children / SIGCONT all children : } transcodeLauncher() { checkLoad checkFinished #until running processes < max processes #do checkLoad checkFinished readUserInput #done transcodeFile & #update counter / metadata } #UI if [ ! -f ~/.atom/atom.cfg ] then if [ ! -d ~/.atom ] then mkdir -p ~/.atom fi sed "s:%HOME%:$HOME:" "$exampleconf" > ~/.atom/atom.cfg cat >&2 <<-EOCfgNotice No configuration file found! An example file has been created as ~/.atom/atom.cfg. You should change it to your likings using you favorite editor. Bailing out. EOCfgNotice exit $ENOCFG fi getConfig { cat <&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 NOT destination_files.last_change = source_files.last_change AND NOT tags.last_change = source_files.last_change AND mime_type_actions.action = 1; SELECT "AtOM:NoMoreFiles";' >&3 read -u4 line while ! [[ $line = AtOM:NoMoreFiles ]] do tagfiles+=("$line") read -u4 line done echo 'BEGIN TRANSACTION;' >&3 for line in "${tagfiles[@]}" do sourcefileid=${line%%|*} rest=${line#*|} lastchange=${rest%%|*} rest=${rest#*|} mimetype=${rest%%|*} filename=${rest#*|} echo -en "\rTags: $((++count*100/filecount))%" if getTags then Update tags \ album "$album" \ albumartist "$albumartist" \ artist "$artist" \ composer "$composer" \ disc "$disc" \ genre "$genre" \ performer "$performer" \ title "$title" \ track "$tracknum" \ year "$year" \ last_change "$lastchange" \ rate "$rate" \ channels "$channels" \ >/dev/null <<<"source_file = $sourcefileid" unset genre \ albumartist \ year \ album \ disc \ artist \ tracknum \ title \ composer \ performer \ rate \ channels fi done echo 'COMMIT;' >&3 echo -e "\rRead tags from $count files." unset count tagfiles echo ' CREATE TEMPORARY TABLE tasks( id INTEGER PRIMARY KEY, command_line TEXT, requires INTEGER, status INTEGER NOT NULL, FOREIGN KEY(requires) REFERENCES tasks(id) ON DELETE SET NULL );' >&3 closeDatabase # vim:set ts=8 sw=8: